From 1b8fc3d21507078db8ba0503767f982617ce338b Mon Sep 17 00:00:00 2001 From: fisenko Date: Mon, 20 Oct 2025 20:00:28 +0300 Subject: [PATCH] fix: fix upload bug 3d --- src/shared/lib/gltfCacheManager.ts | 98 +++++++++ src/shared/lib/index.ts | 1 + src/shared/modals/UploadMediaDialog/index.tsx | 58 +++++- src/shared/store/ModelLoadingStore/index.ts | 101 +++++++++ src/shared/ui/LoadingSpinner/index.tsx | 143 +++++++++++++ src/shared/ui/ModelLoadingIndicator/index.tsx | 196 ++++++++++++++++++ src/widgets/MediaViewer/ThreeView.tsx | 105 +++++++++- src/widgets/MediaViewer/index.tsx | 27 ++- tsconfig.tsbuildinfo | 2 +- 9 files changed, 722 insertions(+), 9 deletions(-) create mode 100644 src/shared/lib/gltfCacheManager.ts create mode 100644 src/shared/store/ModelLoadingStore/index.ts create mode 100644 src/shared/ui/LoadingSpinner/index.tsx create mode 100644 src/shared/ui/ModelLoadingIndicator/index.tsx diff --git a/src/shared/lib/gltfCacheManager.ts b/src/shared/lib/gltfCacheManager.ts new file mode 100644 index 0000000..ad568fc --- /dev/null +++ b/src/shared/lib/gltfCacheManager.ts @@ -0,0 +1,98 @@ +/** + * Утилита для управления кешем GLTF и blob URL + */ + +// Динамический импорт useGLTF для избежания проблем с SSR +let useGLTF: any = null; + +const initializeUseGLTF = async () => { + if (!useGLTF) { + try { + const drei = await import("@react-three/drei"); + useGLTF = drei.useGLTF; + } catch (error) { + console.warn( + "⚠️ GLTFCacheManager: Не удалось импортировать useGLTF", + error + ); + } + } + return useGLTF; +}; + +/** + * Очищает кеш GLTF для конкретного URL + */ +export const clearGLTFCacheForUrl = async (url: string) => { + try { + const gltf = await initializeUseGLTF(); + if (gltf && gltf.clear) { + gltf.clear(url); + console.log("🧹 GLTFCacheManager: Очистка кеша для URL", { url }); + } + } catch (error) { + console.warn("⚠️ GLTFCacheManager: Ошибка при очистке кеша для URL", error); + } +}; + +/** + * Очищает весь кеш GLTF + */ +export const clearAllGLTFCache = async () => { + try { + const gltf = await initializeUseGLTF(); + if (gltf && gltf.clear) { + gltf.clear(); + console.log("🧹 GLTFCacheManager: Очистка всего кеша GLTF"); + } + } catch (error) { + console.warn("⚠️ GLTFCacheManager: Ошибка при очистке всего кеша", error); + } +}; + +/** + * Очищает blob URL из памяти браузера + */ +export const revokeBlobURL = (url: string) => { + if (url && url.startsWith("blob:")) { + try { + URL.revokeObjectURL(url); + console.log("🧹 GLTFCacheManager: Отзыв blob URL", { url }); + } catch (error) { + console.warn("⚠️ GLTFCacheManager: Ошибка при отзыве blob URL", error); + } + } +}; + +/** + * Комплексная очистка: blob URL + кеш GLTF + */ +export const clearBlobAndGLTFCache = async (url: string) => { + // Сначала отзываем blob URL + revokeBlobURL(url); + + // Затем очищаем кеш GLTF + await clearGLTFCacheForUrl(url); + + console.log("🧹 GLTFCacheManager: Комплексная очистка выполнена", { url }); +}; + +/** + * Очистка при смене медиа (для предотвращения конфликтов) + */ +export const clearMediaTransitionCache = async ( + previousMediaId: string | number | null, + newMediaId: string | number | null, + newMediaType?: number +) => { + console.log("🔄 GLTFCacheManager: Очистка кеша при смене медиа", { + previousMediaId, + newMediaId, + newMediaType, + }); + + // Если переключаемся с/на 3D модель, очищаем весь кеш + if (newMediaType === 6 || previousMediaId) { + await clearAllGLTFCache(); + } +}; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index b633647..e21c768 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -1,5 +1,6 @@ export * from "./mui/theme"; export * from "./DecodeJWT"; +export * from "./gltfCacheManager"; /** * Генерирует название медиа по умолчанию в разных форматах diff --git a/src/shared/modals/UploadMediaDialog/index.tsx b/src/shared/modals/UploadMediaDialog/index.tsx index fdfa1a7..c85f6d3 100644 --- a/src/shared/modals/UploadMediaDialog/index.tsx +++ b/src/shared/modals/UploadMediaDialog/index.tsx @@ -3,9 +3,10 @@ import { MEDIA_TYPE_VALUES, editSightStore, generateDefaultMediaName, + clearBlobAndGLTFCache, } from "@shared"; import { observer } from "mobx-react-lite"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { Dialog, DialogTitle, @@ -82,18 +83,41 @@ export const UploadMediaDialog = observer( [] ); 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); - setMediaUrl(URL.createObjectURL(initialFile)); + 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); @@ -211,10 +235,24 @@ export const UploadMediaDialog = observer( useEffect(() => { if (mediaFile) { - setMediaUrl(URL.createObjectURL(mediaFile as Blob)); + // Очищаем предыдущий 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); // Сбрасываем состояние загрузки при смене файла + console.log("🆕 UploadMediaDialog: Создан новый blob URL", { + newBlobUrl, + fileName: mediaFile.name, + }); } - }, [mediaFile]); + }, [mediaFile]); // Убираем mediaUrl из зависимостей чтобы избежать зацикливания // const fileFormat = useEffect(() => { // const handleKeyPress = (event: KeyboardEvent) => { @@ -263,8 +301,20 @@ export const UploadMediaDialog = observer( }; 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(); }; diff --git a/src/shared/store/ModelLoadingStore/index.ts b/src/shared/store/ModelLoadingStore/index.ts new file mode 100644 index 0000000..a55906b --- /dev/null +++ b/src/shared/store/ModelLoadingStore/index.ts @@ -0,0 +1,101 @@ +import { makeAutoObservable } from "mobx"; + +export interface ModelLoadingState { + isLoading: boolean; + progress: number; + modelId: string | null; + error?: string; + startTime?: number; +} + +class ModelLoadingStore { + private loadingStates: Map = new Map(); + + constructor() { + makeAutoObservable(this); + } + + // Начать отслеживание загрузки модели + startLoading(modelId: string) { + this.loadingStates.set(modelId, { + isLoading: true, + progress: 0, + modelId, + startTime: Date.now(), + }); + } + + // Обновить прогресс загрузки + updateProgress(modelId: string, progress: number) { + const state = this.loadingStates.get(modelId); + if (state) { + state.progress = Math.min(100, Math.max(0, progress)); + } + } + + // Завершить загрузку модели + finishLoading(modelId: string) { + const state = this.loadingStates.get(modelId); + if (state) { + state.isLoading = false; + state.progress = 100; + } + } + + // Остановить загрузку (в случае ошибки) + stopLoading(modelId: string) { + this.loadingStates.delete(modelId); + } + + // Обработать ошибку загрузки + handleError(modelId: string, error?: string) { + const state = this.loadingStates.get(modelId); + if (state) { + state.isLoading = false; + state.error = error || "Ошибка загрузки модели"; + } + } + + // Получить состояние загрузки для конкретной модели + getLoadingState(modelId: string): ModelLoadingState | undefined { + return this.loadingStates.get(modelId); + } + + // Проверить, загружается ли какая-либо модель + get isAnyModelLoading(): boolean { + return Array.from(this.loadingStates.values()).some( + (state) => state.isLoading + ); + } + + // Получить все загружающиеся модели + get loadingModels(): ModelLoadingState[] { + return Array.from(this.loadingStates.values()).filter( + (state) => state.isLoading + ); + } + + // Получить общий прогресс всех загружающихся моделей + get overallProgress(): number { + const loadingModels = this.loadingModels; + if (loadingModels.length === 0) return 100; + + const totalProgress = loadingModels.reduce( + (sum, model) => sum + model.progress, + 0 + ); + return Math.round(totalProgress / loadingModels.length); + } + + // Проверить, заблокировано ли сохранение (есть ли загружающиеся модели) + get isSaveBlocked(): boolean { + return this.isAnyModelLoading; + } + + // Очистить все состояния загрузки + clearAll() { + this.loadingStates.clear(); + } +} + +export const modelLoadingStore = new ModelLoadingStore(); diff --git a/src/shared/ui/LoadingSpinner/index.tsx b/src/shared/ui/LoadingSpinner/index.tsx new file mode 100644 index 0000000..7c00677 --- /dev/null +++ b/src/shared/ui/LoadingSpinner/index.tsx @@ -0,0 +1,143 @@ +import React from "react"; +import { + Box, + CircularProgress, + Typography, + LinearProgress, +} from "@mui/material"; + +interface LoadingSpinnerProps { + progress?: number; + message?: string; + size?: number; + color?: "primary" | "secondary" | "error" | "info" | "success" | "warning"; + variant?: "circular" | "linear"; + showPercentage?: boolean; + thickness?: number; + className?: string; +} + +export const LoadingSpinner: React.FC = ({ + progress, + message = "Загрузка...", + size = 40, + color = "primary", + variant = "circular", + showPercentage = true, + thickness = 4, + className, +}) => { + if (variant === "linear") { + return ( + + + + {showPercentage && progress !== undefined && ( + + {`${Math.round(progress)}%`} + + )} + + {message && ( + + {message} + + )} + + ); + } + + return ( + + + + {showPercentage && progress !== undefined && ( + + + {`${Math.round(progress)}%`} + + + )} + + {message && ( + + {message} + + )} + + ); +}; diff --git a/src/shared/ui/ModelLoadingIndicator/index.tsx b/src/shared/ui/ModelLoadingIndicator/index.tsx new file mode 100644 index 0000000..1033a41 --- /dev/null +++ b/src/shared/ui/ModelLoadingIndicator/index.tsx @@ -0,0 +1,196 @@ +import React from "react"; +import { + Box, + Typography, + LinearProgress, + CircularProgress, +} from "@mui/material"; + +interface ModelLoadingIndicatorProps { + progress?: number; + message?: string; + isVisible?: boolean; + variant?: "overlay" | "inline"; + size?: "small" | "medium" | "large"; + showDetails?: boolean; +} + +export const ModelLoadingIndicator: React.FC = ({ + progress = 0, + message = "Загрузка 3D модели...", + isVisible = true, + variant = "overlay", + size = "medium", + showDetails = true, +}) => { + const sizeConfig = { + small: { + spinnerSize: 32, + fontSize: "0.75rem", + progressBarWidth: 150, + padding: 2, + }, + medium: { + spinnerSize: 48, + fontSize: "0.875rem", + progressBarWidth: 200, + padding: 3, + }, + large: { + spinnerSize: 64, + fontSize: "1rem", + progressBarWidth: 250, + padding: 4, + }, + }; + + const currentSize = sizeConfig[size]; + + if (!isVisible) return null; + + const getProgressStage = (progress: number): string => { + if (progress < 20) return "Инициализация..."; + if (progress < 40) return "Загрузка геометрии..."; + if (progress < 60) return "Обработка материалов..."; + if (progress < 80) return "Загрузка текстур..."; + if (progress < 95) return "Финализация..."; + if (progress === 100) return "Готово!"; + return "Загрузка..."; + }; + + const content = ( + + {/* Крутяшка с процентами */} + + + + + {`${Math.round(progress)}%`} + + + + + {/* Линейный прогресс бар */} + + + + + {/* Основное сообщение */} + + {message} + + + {/* Детальная информация о прогрессе */} + {showDetails && progress > 0 && ( + + {getProgressStage(progress)} + + )} + + ); + + if (variant === "overlay") { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +}; diff --git a/src/widgets/MediaViewer/ThreeView.tsx b/src/widgets/MediaViewer/ThreeView.tsx index 39d03aa..fbd9573 100644 --- a/src/widgets/MediaViewer/ThreeView.tsx +++ b/src/widgets/MediaViewer/ThreeView.tsx @@ -3,6 +3,78 @@ import { OrbitControls, Stage, useGLTF } from "@react-three/drei"; import { useEffect, Suspense } from "react"; import { Box, CircularProgress, Typography } from "@mui/material"; +// Утилита для очистки кеша GLTF +const clearGLTFCache = (url?: string) => { + try { + if (url) { + // Если это blob URL, очищаем его из кеша + if (url.startsWith("blob:")) { + useGLTF.clear(url); + console.log("🧹 clearGLTFCache: Очистка blob URL из кеша GLTF", { + url, + }); + } else { + useGLTF.clear(url); + console.log("🧹 clearGLTFCache: Очистка кеша для URL", { url }); + } + } else { + // Очищаем весь кеш GLTF + useGLTF.clear(); + console.log("🧹 clearGLTFCache: Очистка всего кеша GLTF"); + } + } catch (error) { + console.warn("⚠️ clearGLTFCache: Ошибка при очистке кеша", error); + } +}; + +// Утилита для проверки типа файла +const isValid3DFile = (url: string): boolean => { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname.toLowerCase(); + const searchParams = urlObj.searchParams; + + // Проверяем расширение файла в пути + const validExtensions = [".glb", ".gltf"]; + const hasValidExtension = validExtensions.some((ext) => + pathname.endsWith(ext) + ); + + // Проверяем параметры запроса на наличие типа файла + const fileType = searchParams.get("type") || searchParams.get("format"); + const hasValidType = + fileType && ["glb", "gltf"].includes(fileType.toLowerCase()); + + // Если это blob URL, считаем его валидным (пользователь выбрал файл) + const isBlobUrl = url.startsWith("blob:"); + + // Если это URL с токеном и нет явного расширения, считаем валидным + // (предполагаем что сервер вернет правильный файл) + const hasToken = searchParams.has("token"); + const isServerUrl = hasToken && !hasValidExtension; + + const isValid = + hasValidExtension || hasValidType || isBlobUrl || isServerUrl; + + console.log("🔍 isValid3DFile: Проверка файла", { + url, + pathname, + hasValidExtension, + fileType, + hasValidType, + isBlobUrl, + isServerUrl, + isValid, + }); + + return isValid; + } catch (error) { + console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error); + // В случае ошибки парсинга URL, считаем валидным (пусть useGLTF сам разберется) + return true; + } +}; + type ModelViewerProps = { width?: string; fileUrl: string; @@ -10,6 +82,18 @@ type ModelViewerProps = { }; const Model = ({ fileUrl }: { fileUrl: string }) => { + // Очищаем кеш перед загрузкой новой модели + useEffect(() => { + // Очищаем кеш для текущего URL + clearGLTFCache(fileUrl); + }, [fileUrl]); + + // Проверяем валидность файла перед загрузкой (только для blob URL) + if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) { + console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl }); + throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`); + } + const { scene } = useGLTF(fileUrl); return ; }; @@ -49,11 +133,26 @@ export const ThreeView = ({ height = "100%", width = "100%", }: ModelViewerProps) => { - // Очищаем кеш при размонтировании + // Проверяем валидность файла (только для blob URL) useEffect(() => { + if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) { + console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl }); + } + }, [fileUrl]); + + // Очищаем кеш при размонтировании и при смене URL + useEffect(() => { + // Очищаем кеш сразу при монтировании компонента + clearGLTFCache(fileUrl); + console.log("🧹 ThreeView: Очистка кеша модели при монтировании", { + fileUrl, + }); + return () => { - useGLTF.clear(fileUrl); - console.log("🧹 ThreeView: Очистка кеша модели", { fileUrl }); + clearGLTFCache(fileUrl); + console.log("🧹 ThreeView: Очистка кеша модели при размонтировании", { + fileUrl, + }); }; }, [fileUrl]); diff --git a/src/widgets/MediaViewer/index.tsx b/src/widgets/MediaViewer/index.tsx index 3ebd483..a7a8cdb 100644 --- a/src/widgets/MediaViewer/index.tsx +++ b/src/widgets/MediaViewer/index.tsx @@ -1,9 +1,10 @@ import { Box } from "@mui/material"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"; import { ThreeView } from "./ThreeView"; import { ThreeViewErrorBoundary } from "./ThreeViewErrorBoundary"; +import { clearMediaTransitionCache } from "../../shared/lib/gltfCacheManager"; export interface MediaData { id: string | number; @@ -28,6 +29,30 @@ export function MediaViewer({ }>) { const token = localStorage.getItem("token"); const [resetKey, setResetKey] = useState(0); + const [previousMediaId, setPreviousMediaId] = useState< + string | number | null + >(null); + + // Сбрасываем ключ при смене медиа + useEffect(() => { + if (media?.id !== previousMediaId) { + console.log("🔄 MediaViewer: Смена медиа, сброс ключа", { + previousMediaId, + newMediaId: media?.id, + mediaType: media?.media_type, + }); + + // Используем новый cache manager для очистки кеша + clearMediaTransitionCache( + previousMediaId, + media?.id || null, + media?.media_type + ); + + setResetKey(0); + setPreviousMediaId(media?.id || null); + } + }, [media?.id, media?.media_type, previousMediaId]); const handleReset = () => { console.log("🔄 MediaViewer: handleReset вызван", { diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index cb8efe7..1e76cf9 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/carriersvg.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/globalerrorboundary.tsx","./src/app/index.tsx","./src/app/router/index.tsx","./src/entities/index.ts","./src/entities/navigation/index.ts","./src/entities/navigation/model/index.ts","./src/entities/navigation/ui/index.tsx","./src/features/index.ts","./src/features/navigation/index.ts","./src/features/navigation/ui/index.tsx","./src/pages/index.ts","./src/pages/article/index.ts","./src/pages/article/articlecreatepage/index.tsx","./src/pages/article/articleeditpage/index.tsx","./src/pages/article/articlelistpage/index.tsx","./src/pages/article/articlepreviewpage/previewleftwidget.tsx","./src/pages/article/articlepreviewpage/previewrightwidget.tsx","./src/pages/article/articlepreviewpage/index.tsx","./src/pages/carrier/index.ts","./src/pages/carrier/carriercreatepage/index.tsx","./src/pages/carrier/carriereditpage/index.tsx","./src/pages/carrier/carrierlistpage/index.tsx","./src/pages/city/index.ts","./src/pages/city/citycreatepage/index.tsx","./src/pages/city/cityeditpage/index.tsx","./src/pages/city/citylistpage/index.tsx","./src/pages/city/citypreviewpage/index.tsx","./src/pages/country/index.ts","./src/pages/country/countryaddpage/index.tsx","./src/pages/country/countrycreatepage/index.tsx","./src/pages/country/countryeditpage/index.tsx","./src/pages/country/countrylistpage/index.tsx","./src/pages/country/countrypreviewpage/index.tsx","./src/pages/createsightpage/index.tsx","./src/pages/devicespage/index.tsx","./src/pages/editsightpage/index.tsx","./src/pages/loginpage/index.tsx","./src/pages/mainpage/index.tsx","./src/pages/mappage/index.tsx","./src/pages/mappage/mapstore.ts","./src/pages/media/index.ts","./src/pages/media/mediacreatepage/index.tsx","./src/pages/media/mediaeditpage/index.tsx","./src/pages/media/medialistpage/index.tsx","./src/pages/media/mediapreviewpage/index.tsx","./src/pages/route/linekedstations.tsx","./src/pages/route/index.ts","./src/pages/route/routecreatepage/index.tsx","./src/pages/route/routeeditpage/index.tsx","./src/pages/route/routelistpage/index.tsx","./src/pages/route/route-preview/constants.ts","./src/pages/route/route-preview/infinitecanvas.tsx","./src/pages/route/route-preview/leftsidebar.tsx","./src/pages/route/route-preview/mapdatacontext.tsx","./src/pages/route/route-preview/rightsidebar.tsx","./src/pages/route/route-preview/sight.tsx","./src/pages/route/route-preview/sightinfowidget.tsx","./src/pages/route/route-preview/station.tsx","./src/pages/route/route-preview/transformcontext.tsx","./src/pages/route/route-preview/travelpath.tsx","./src/pages/route/route-preview/widgets.tsx","./src/pages/route/route-preview/index.tsx","./src/pages/route/route-preview/types.ts","./src/pages/route/route-preview/utils.ts","./src/pages/sight/index.ts","./src/pages/sight/sightlistpage/index.tsx","./src/pages/sightpage/index.tsx","./src/pages/snapshot/index.ts","./src/pages/snapshot/snapshotcreatepage/index.tsx","./src/pages/snapshot/snapshotlistpage/index.tsx","./src/pages/station/linkedsights.tsx","./src/pages/station/index.ts","./src/pages/station/stationcreatepage/index.tsx","./src/pages/station/stationeditpage/index.tsx","./src/pages/station/stationlistpage/index.tsx","./src/pages/station/stationpreviewpage/index.tsx","./src/pages/user/index.ts","./src/pages/user/usercreatepage/index.tsx","./src/pages/user/usereditpage/index.tsx","./src/pages/user/userlistpage/index.tsx","./src/pages/vehicle/index.ts","./src/pages/vehicle/vehiclecreatepage/index.tsx","./src/pages/vehicle/vehicleeditpage/index.tsx","./src/pages/vehicle/vehiclelistpage/index.tsx","./src/pages/vehicle/vehiclepreviewpage/index.tsx","./src/shared/index.tsx","./src/shared/api/index.tsx","./src/shared/config/carriersvg.tsx","./src/shared/config/constants.tsx","./src/shared/config/index.ts","./src/shared/const/index.ts","./src/shared/const/mediatypes.ts","./src/shared/hooks/index.ts","./src/shared/hooks/useselectedcity.ts","./src/shared/lib/gltfcachemanager.ts","./src/shared/lib/index.ts","./src/shared/lib/decodejwt/index.ts","./src/shared/lib/mui/theme.ts","./src/shared/modals/index.ts","./src/shared/modals/previewmediadialog/index.tsx","./src/shared/modals/selectarticledialog/index.tsx","./src/shared/modals/selectmediadialog/index.tsx","./src/shared/modals/uploadmediadialog/index.tsx","./src/shared/store/index.ts","./src/shared/store/articlesstore/index.tsx","./src/shared/store/authstore/index.tsx","./src/shared/store/carrierstore/index.tsx","./src/shared/store/citystore/index.ts","./src/shared/store/countrystore/index.ts","./src/shared/store/createsightstore/index.tsx","./src/shared/store/devicesstore/index.tsx","./src/shared/store/editsightstore/index.tsx","./src/shared/store/languagestore/index.tsx","./src/shared/store/mediastore/index.tsx","./src/shared/store/menustore/index.ts","./src/shared/store/modelloadingstore/index.ts","./src/shared/store/routestore/index.ts","./src/shared/store/selectedcitystore/index.ts","./src/shared/store/sightsstore/index.tsx","./src/shared/store/snapshotstore/index.ts","./src/shared/store/stationsstore/index.ts","./src/shared/store/userstore/index.ts","./src/shared/store/vehiclestore/index.ts","./src/shared/ui/index.ts","./src/shared/ui/backbutton/index.tsx","./src/shared/ui/coordinatesinput/index.tsx","./src/shared/ui/input/index.tsx","./src/shared/ui/loadingspinner/index.tsx","./src/shared/ui/modal/index.tsx","./src/shared/ui/modelloadingindicator/index.tsx","./src/shared/ui/tabpanel/index.tsx","./src/widgets/index.ts","./src/widgets/cityselector/index.tsx","./src/widgets/createbutton/index.tsx","./src/widgets/deletemodal/index.tsx","./src/widgets/devicestable/index.tsx","./src/widgets/imageuploadcard/index.tsx","./src/widgets/languageswitcher/index.tsx","./src/widgets/layout/index.tsx","./src/widgets/layout/ui/appbar.tsx","./src/widgets/layout/ui/drawer.tsx","./src/widgets/layout/ui/drawerheader.tsx","./src/widgets/leaveagree/index.tsx","./src/widgets/mediaarea/index.tsx","./src/widgets/mediaareaforsight/index.tsx","./src/widgets/mediaviewer/threeview.tsx","./src/widgets/mediaviewer/threeviewerrorboundary.tsx","./src/widgets/mediaviewer/index.tsx","./src/widgets/modelviewer3d/index.tsx","./src/widgets/reactmarkdown/index.tsx","./src/widgets/reactmarkdowneditor/index.tsx","./src/widgets/savewithoutcityagree/index.tsx","./src/widgets/sightedit/index.tsx","./src/widgets/sightheader/index.ts","./src/widgets/sightheader/ui/index.tsx","./src/widgets/sighttabs/index.ts","./src/widgets/sighttabs/createinformationtab/mediauploadbox.tsx","./src/widgets/sighttabs/createinformationtab/index.tsx","./src/widgets/sighttabs/createlefttab/index.tsx","./src/widgets/sighttabs/createrighttab/index.tsx","./src/widgets/sighttabs/informationtab/index.tsx","./src/widgets/sighttabs/leftwidgettab/index.tsx","./src/widgets/sighttabs/rightwidgettab/index.tsx","./src/widgets/sightstable/index.tsx","./src/widgets/snapshotrestore/index.tsx","./src/widgets/videopreviewcard/index.tsx","./src/widgets/modals/editstationmodal.tsx","./src/widgets/modals/index.ts","./src/widgets/modals/selectarticledialog/index.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file