@@ -5,11 +5,16 @@ import { Box, Tooltip, Typography } from '@mui/material' import { getFileTypeIcon, isImageFile } from '#/shared/lib/helpers' import styles from './load_image.module.scss' +const isVideoFile = (file: File) => { + const name = (file.name || '').toLowerCase().split('?')[0] + return file.type === 'video/mp4' || name.endsWith('.mp4') +} + interface IProps { loading: boolean unpinImage?: () => void image: File | null | undefined - imageLoad?: React.ChangeEventHandler + imageLoad?: (event: React.ChangeEvent | null, file?: File) => void types: string[] fileNameMaxLength?: number desktop?: boolean @@ -22,6 +27,7 @@ const acceptTypes: any = { docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', doc: 'application/msword', audio: 'audio/*', + video: 'video/mp4,.mp4', txt: '.txt', text: '', } @@ -47,6 +53,7 @@ export const LoadImage = ({ loading, unpinImage, image, imageLoad, types, fileNa const [inputTypes, setInputTypes] = useState('') const [previewUrl, setPreviewUrl] = useState(null) const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null) + const videoThumbTimeRef = useRef(0.05) React.useEffect(() => { setInputTypes(types.map((el) => acceptTypes[el]).toString()) @@ -56,31 +63,73 @@ export const LoadImage = ({ loading, unpinImage, image, imageLoad, types, fileNa if (!image) { setPreviewUrl(null) setDimensions(null) + videoThumbTimeRef.current = 0.05 return } - if (!isImageFile(image)) { + const isImage = isImageFile(image) + const isVideo = isVideoFile(image) + + if (!isImage && !isVideo) { setPreviewUrl(null) setDimensions(null) + videoThumbTimeRef.current = 0.05 return } + const url = URL.createObjectURL(image) setPreviewUrl(url) - const img = new window.Image() - img.onload = () => { - setDimensions({ width: img.naturalWidth, height: img.naturalHeight }) + if (isImage) { + const img = new window.Image() + img.onload = () => { + setDimensions({ width: img.naturalWidth, height: img.naturalHeight }) + } + img.src = url + } else { + setDimensions(null) + videoThumbTimeRef.current = 0.05 } - img.src = url return () => URL.revokeObjectURL(url) }, [image]) + const seekVideoToFirstFrame = (el: HTMLVideoElement | null) => { + if (!el) return + const onLoadedMetadata = () => { + try { + const duration = Number.isFinite(el.duration) ? el.duration : 0 + const target = duration > 0 ? Math.min(0.1, duration / 2) : 0.05 + videoThumbTimeRef.current = target + el.currentTime = target + el.pause() + } catch {} + } + el.addEventListener('loadedmetadata', onLoadedMetadata, { once: true }) + } + + const startTooltipVideo = async (el: HTMLVideoElement | null) => { + if (!el) return + try { + // начинаем чуть дальше нулевого кадра (иначе часто чёрный) + const duration = Number.isFinite(el.duration) ? el.duration : 0 + const target = duration > 0 ? Math.min(0.1, duration / 2) : 0.05 + el.currentTime = target + } catch {} + try { + el.muted = true + await el.play() + } catch { + // autoplay может быть заблокирован политикой браузера + } + } + if (!imageLoad || loading) { return null } if (image) { const isImage = isImageFile(image) + const isVideo = isVideoFile(image) const fileIcon = getFileTypeIcon(image) const isSvg = image.name.toLowerCase().split('?')[0].endsWith('.svg') @@ -88,6 +137,17 @@ export const LoadImage = ({ loading, unpinImage, image, imageLoad, types, fileNa {isImage && previewUrl ? ( Превью + ) : isVideo && previewUrl && desktop ? ( +