feature/webgl-preview-update (#21)

Co-authored-by: itoshi <kkzemeow@gmail.com>
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2026-10-13 16:49:18 +00:00
parent 50ad374cf5
commit 71834904a1
327 changed files with 39034 additions and 8829 deletions

View File

@@ -4,9 +4,13 @@ import {
editSightStore,
generateDefaultMediaName,
clearBlobAndGLTFCache,
authStore,
snapshotStore,
PreviewBackgroundToggle,
} from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState, useRef } from "react";
import { toast } from "react-toastify";
import {
Dialog,
DialogTitle,
@@ -39,6 +43,8 @@ interface UploadMediaDialogProps {
afterUploadSight?: (id: string) => void;
hardcodeType?:
| "thumbnail"
| "icon"
| "alt_icon"
| "watermark_lu"
| "watermark_rd"
| "image"
@@ -51,10 +57,12 @@ interface UploadMediaDialogProps {
| "carrier"
| "country"
| "vehicle"
| "station";
| "station"
| "route"
| "user";
isArticle?: boolean;
articleName?: string;
initialFile?: File; // <--- добавлено
initialFile?: File;
}
export const UploadMediaDialog = observer(
@@ -68,7 +76,7 @@ export const UploadMediaDialog = observer(
isArticle,
articleName,
initialFile, // <--- добавлено
initialFile,
}: UploadMediaDialogProps) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -83,11 +91,12 @@ export const UploadMediaDialog = observer(
[]
);
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
const [isPreviewBackgroundDark, setIsPreviewBackgroundDark] =
useState(false);
const previousMediaUrlRef = useRef<string | null>(null);
useEffect(() => {
if (initialFile) {
// Очищаем предыдущий blob URL если он существует
if (
previousMediaUrlRef.current &&
previousMediaUrlRef.current.startsWith("blob:")
@@ -97,8 +106,26 @@ export const UploadMediaDialog = observer(
setMediaFile(initialFile);
setMediaFilename(initialFile.name);
setAvailableMediaTypes([2]);
setMediaType(2);
const extension = initialFile.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);
}
}
const newBlobUrl = URL.createObjectURL(initialFile);
setMediaUrl(newBlobUrl);
previousMediaUrlRef.current = newBlobUrl;
@@ -106,7 +133,6 @@ export const UploadMediaDialog = observer(
}
}, [initialFile]);
// Очистка blob URL при размонтировании компонента
useEffect(() => {
return () => {
if (
@@ -116,13 +142,13 @@ export const UploadMediaDialog = observer(
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)) {
@@ -134,22 +160,18 @@ export const UploadMediaDialog = observer(
extension
)
) {
// Для изображений доступны все типы кроме видео
setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель
setMediaType(1); // По умолчанию Фото
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,
@@ -157,10 +179,9 @@ export const UploadMediaDialog = observer(
true
);
} else if (contextObjectName && contextObjectName.trim() !== "") {
// Для обычных медиа с названием объекта
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: 1; // По умолчанию фото
: 1;
defaultName = generateDefaultMediaName(
contextObjectName,
fileToUpload.name,
@@ -168,10 +189,9 @@ export const UploadMediaDialog = observer(
false
);
} else {
// Для медиа без названия объекта
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: 1; // По умолчанию фото
: 1;
defaultName = generateDefaultMediaName(
"",
fileToUpload.name,
@@ -185,13 +205,11 @@ export const UploadMediaDialog = observer(
}
}, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]);
// Обновляем название при изменении типа медиа
useEffect(() => {
if (mediaFilename && mediaType > 0) {
let defaultName = "";
if (isArticle && articleName && contextObjectName) {
// Для статей: "Название достопримечательности_название файла_название статьи"
defaultName = generateDefaultMediaName(
contextObjectName,
mediaFilename,
@@ -199,7 +217,6 @@ export const UploadMediaDialog = observer(
true
);
} else if (contextObjectName && contextObjectName.trim() !== "") {
// Для обычных медиа с названием объекта
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: mediaType;
@@ -210,7 +227,6 @@ export const UploadMediaDialog = observer(
false
);
} else {
// Для медиа без названия объекта
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: mediaType;
@@ -235,7 +251,6 @@ export const UploadMediaDialog = observer(
useEffect(() => {
if (mediaFile) {
// Очищаем предыдущий blob URL и кеш GLTF если он существует
if (
previousMediaUrlRef.current &&
previousMediaUrlRef.current.startsWith("blob:")
@@ -245,22 +260,10 @@ export const UploadMediaDialog = observer(
const newBlobUrl = URL.createObjectURL(mediaFile as Blob);
setMediaUrl(newBlobUrl);
previousMediaUrlRef.current = newBlobUrl; // Сохраняем новый URL в ref
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
previousMediaUrlRef.current = newBlobUrl;
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]);
}, [mediaFile]);
const handleSave = async () => {
if (!mediaFile) return;
@@ -268,12 +271,16 @@ export const UploadMediaDialog = observer(
setIsLoading(true);
setError(null);
const uploadStartTime = Date.now();
try {
const effectiveMediaType = hardcodeType
? (MEDIA_TYPE_VALUES[hardcodeType] as number)
: mediaType;
const media = await uploadMedia(
mediaFilename,
hardcodeType
? (MEDIA_TYPE_VALUES[hardcodeType] as number)
: mediaType,
effectiveMediaType,
mediaFile,
mediaName
);
@@ -284,11 +291,45 @@ export const UploadMediaDialog = observer(
await afterUpload(media);
}
}
if (effectiveMediaType === 2) {
const uploadDurationSec = Math.round((Date.now() - uploadStartTime) / 1000);
const minutes = Math.floor(uploadDurationSec / 60);
const seconds = uploadDurationSec % 60;
const durationStr = minutes > 0
? `${minutes} мин ${seconds} сек`
: `${seconds} сек`;
const fileSizeMb = mediaFile.size / (1024 * 1024);
const fileSizeStr = fileSizeMb >= 1024
? `${(fileSizeMb / 1024).toFixed(2)} ГБ`
: `${fileSizeMb.toFixed(1)} МБ`;
if (authStore.canRead("snapshots")) {
try {
await snapshotStore.getStorageInfo();
const storage = snapshotStore.storageInfo;
if (storage) {
toast.success(
`Видео (${fileSizeStr}) загружено за ${durationStr}. Свободно на диске: ${storage.available_disk_space_gb.toFixed(2)} ГБ из ${storage.total_disk_space_gb.toFixed(2)} ГБ`,
{ autoClose: 8000 }
);
} else {
toast.success(`Видео (${fileSizeStr}) загружено за ${durationStr}`, { autoClose: 6000 });
}
} catch {
toast.success(`Видео (${fileSizeStr}) загружено за ${durationStr}`, { autoClose: 6000 });
}
} else {
toast.success(`Видео (${fileSizeStr}) загружено за ${durationStr}`, { autoClose: 6000 });
}
}
setSuccess(true);
// Закрываем модальное окно после успешного сохранения
setTimeout(() => {
handleClose();
}, 1000); // Небольшая задержка, чтобы пользователь увидел сообщение об успехе
}, 1000);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save media");
} finally {
@@ -297,7 +338,6 @@ export const UploadMediaDialog = observer(
};
const handleClose = () => {
// Очищаем blob URL и кеш GLTF при закрытии диалога
if (
previousMediaUrlRef.current &&
previousMediaUrlRef.current.startsWith("blob:")
@@ -310,7 +350,8 @@ export const UploadMediaDialog = observer(
setMediaUrl(null);
setMediaFile(null);
setIsPreviewLoaded(false);
previousMediaUrlRef.current = null; // Очищаем ref
setIsPreviewBackgroundDark(false);
previousMediaUrlRef.current = null;
onClose();
};
@@ -379,8 +420,18 @@ export const UploadMediaDialog = observer(
justifyContent: "center",
height: "100%",
position: "relative",
backgroundColor: isPreviewBackgroundDark ? "#000" : "#fff",
transition: "background-color 150ms ease",
}}
>
{mediaUrl && mediaType !== 2 && mediaType !== 6 && (
<PreviewBackgroundToggle
dark={isPreviewBackgroundDark}
onToggle={() =>
setIsPreviewBackgroundDark((value) => !value)
}
/>
)}
{!isPreviewLoaded && mediaUrl && (
<Box
sx={{