Files
WhiteNightsAdminPanel/src/shared/modals/UploadMediaDialog/index.tsx

512 lines
15 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,
authStore,
snapshotStore,
} from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState, useRef } from "react";
import { toast } from "react-toastify";
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"
| "icon"
| "alt_icon"
| "watermark_lu"
| "watermark_rd"
| "image"
| "video_preview"
| null;
contextObjectName?: string;
contextType?:
| "sight"
| "city"
| "carrier"
| "country"
| "vehicle"
| "station"
| "route"
| "user";
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);
const uploadStartTime = Date.now();
try {
const effectiveMediaType = hardcodeType
? (MEDIA_TYPE_VALUES[hardcodeType] as number)
: mediaType;
const media = await uploadMedia(
mediaFilename,
effectiveMediaType,
mediaFile,
mediaName
);
if (media) {
if (afterUploadSight) {
await afterUploadSight(media.id);
} else if (afterUpload) {
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);
} 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>
</>
);
}
);