From 2cf6e0c8427315c5f71942d472310e98cbe11751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B5=D0=BD=D0=B8=D1=81=20=D0=9A=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D0=B2?= Date: Fri, 31 Jan 2025 22:48:00 +0000 Subject: [PATCH] Develop --- package-lock.json | 16 ++ package.json | 1 + src/components/App/index.tsx | 26 +-- .../components/CameraModal/index.tsx | 144 +++++++++++++++++ .../components/CameraModal/styles.module.scss | 54 +++++++ .../PalmistryV1/pages/Camera/index.tsx | 63 ++++++-- .../palm-camera-modal/palm-camera-modal.tsx | 151 ++++++++++++++++-- 7 files changed, 420 insertions(+), 35 deletions(-) create mode 100644 src/components/PalmistryV1/components/CameraModal/index.tsx create mode 100644 src/components/PalmistryV1/components/CameraModal/styles.module.scss diff --git a/package-lock.json b/package-lock.json index 95d9054..1348350 100755 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.2", "react-slick": "^0.30.2", + "react-webcam": "^7.2.0", "sass": "^1.77.6", "slick-carousel": "^1.8.1", "socket.io-client": "^4.8.1", @@ -4681,6 +4682,15 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-webcam": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz", + "integrity": "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==", + "peerDependencies": { + "react": ">=16.2.0", + "react-dom": ">=16.2.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8641,6 +8651,12 @@ "prop-types": "^15.6.2" } }, + "react-webcam": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-webcam/-/react-webcam-7.2.0.tgz", + "integrity": "sha512-xkrzYPqa1ag2DP+2Q/kLKBmCIfEx49bVdgCCCcZf88oF+0NPEbkwYk3/s/C7Zy0mhM8k+hpdNkBLzxg8H0aWcg==", + "requires": {} + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index d64f37f..36fa688 100755 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.2", "react-slick": "^0.30.2", + "react-webcam": "^7.2.0", "sass": "^1.77.6", "slick-carousel": "^1.8.1", "socket.io-client": "^4.8.1", diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 095da4d..23f11a6 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -52,7 +52,7 @@ import HomePage from "../HomePage"; import UserCallbacksPage from "../UserCallbacksPage"; import NavbarFooter, { INavbarHomeItems } from "../NavbarFooter"; import { EPathsFromHome } from "@/store/siteConfig"; -import parseAPNG, { APNG } from "apng-js"; +import { APNG } from "apng-js"; import { useApi, useApiCall } from "@/api"; import { Asset } from "@/api/resources/Assets"; import PaymentResultPage from "../PaymentPage/results"; @@ -145,6 +145,7 @@ if (isProduction) { function App(): JSX.Element { const location = useLocation(); const [leoApng, setLeoApng] = useState(Error); + setLeoApng useScrollToTop({ scrollBehavior: "auto" }); // const [ // padLockApng, @@ -232,6 +233,7 @@ function App(): JSX.Element { }, [api]); const { data } = useApiCall(assetsData); + data // jwt auth useLayoutEffect(() => { @@ -275,17 +277,17 @@ function App(): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, api, token]); - useEffect(() => { - async function getApng() { - if (!data) return; - const response = await fetch( - data.find((item) => item.key === "au.apng.leo")?.url || "" - ); - const arrayBuffer = await response.arrayBuffer(); - setLeoApng(parseAPNG(arrayBuffer)); - } - getApng(); - }, [data]); + // useEffect(() => { + // async function getApng() { + // if (!data) return; + // const response = await fetch( + // data.find((item) => item.key === "au.apng.leo")?.url || "" + // ); + // const arrayBuffer = await response.arrayBuffer(); + // setLeoApng(parseAPNG(arrayBuffer)); + // } + // getApng(); + // }, [data]); // useEffect(() => { // (async () => { diff --git a/src/components/PalmistryV1/components/CameraModal/index.tsx b/src/components/PalmistryV1/components/CameraModal/index.tsx new file mode 100644 index 0000000..6dc5aeb --- /dev/null +++ b/src/components/PalmistryV1/components/CameraModal/index.tsx @@ -0,0 +1,144 @@ +import Webcam from "react-webcam"; +import styles from "./styles.module.scss"; +import ModalOverlay, { ModalOverlayType } from "@/components/palmistry/modal-overlay/modal-overlay"; +import Modal from "@/components/palmistry/modal/modal"; +import { useRef, useState } from "react"; +// import { useDynamicSize } from "@/hooks/useDynamicSize"; + +interface CameraModalProps { + onClose: () => void; + onTakePhoto: (photo: string) => void; + onError: (error: string | DOMException) => void; +} + +function CameraModal({ + onClose, + onTakePhoto, + onError, +}: CameraModalProps) { + const [isVideoReady, setIsVideoReady] = useState(false); + // const { + // width: _width, height: _height, + // elementRef: containerRef } = useDynamicSize({}); + // const [width, height] = [720, 1280]; + // const width = _width * 2; + // const height = _height * 2; + + // const isLandscape = height <= width; + // const ratio = isLandscape ? width / height : height / width; + const cameraRef = useRef(null); + + + const onClickOverlay = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const onClickTakePhoto = () => { + if (!isVideoReady) return onError("Video is not ready"); + const photo = cameraRef.current?.getScreenshot( + // { + // width, + // height, + // } + ); + if (photo) { + onTakePhoto(photo); + } + }; + + return + +
+ {/* {width} {height} */} + + + + + + + + + setIsVideoReady(true)} + onUserMediaError={(error) => { + setIsVideoReady(false); + console.error(error); + onError(error); + }} + /> +
+
+ + ; +} + +export default CameraModal; + diff --git a/src/components/PalmistryV1/components/CameraModal/styles.module.scss b/src/components/PalmistryV1/components/CameraModal/styles.module.scss new file mode 100644 index 0000000..5c79366 --- /dev/null +++ b/src/components/PalmistryV1/components/CameraModal/styles.module.scss @@ -0,0 +1,54 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100dvh; + min-height: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; +} + +// .modal { +// width: 100vw !important; +// height: 100dvh !important; +// margin: 0 !important; +// padding: 0 !important; +// max-width: none !important; +// background: transparent !important; +// } + +.cameraContainer { + width: 100%; + height: 100dvh; + position: relative; + overflow: hidden; +} + +.camera { + width: 100%; + height: 100dvh; + object-fit: cover; +} + +.shutterButton { + background: var(--white); + border-radius: 50%; + bottom: calc(0dvh + 30px); + left: 50%; + padding: 15px; + border: 20px solid hsla(0, 0%, 100%, 0.28); + position: absolute; + -webkit-transform: translate(-50%, 15px); + transform: translate(-50%, 15px); +} + +.handIcon { + position: absolute; + height: calc(100dvh - 120px); + left: 50%; + top: 20px; + transform: translate(-50%); + width: auto; + z-index: 9; +} diff --git a/src/components/PalmistryV1/pages/Camera/index.tsx b/src/components/PalmistryV1/pages/Camera/index.tsx index 8eca7a8..e16fd68 100644 --- a/src/components/PalmistryV1/pages/Camera/index.tsx +++ b/src/components/PalmistryV1/pages/Camera/index.tsx @@ -1,4 +1,3 @@ -import PalmCameraModal from "@/components/palmistry/palm-camera-modal/palm-camera-modal"; import styles from "./styles.module.scss"; import { DataURIToBlob } from "@/services/data"; import { useApi } from "@/api"; @@ -14,6 +13,7 @@ import UploadModal from "@/components/palmistry/upload-modal/upload-modal"; import Toast from "@/components/pages/ABDesign/v1/components/Toast"; import { useTranslations } from "@/hooks/translations"; import { ELocalesPlacement } from "@/locales"; +import CameraModal from "../../components/CameraModal"; const isProduction = import.meta.env.MODE === "production"; @@ -30,6 +30,7 @@ function Camera() { const [isLoading, setIsLoading] = useState(false); const [uploadMenuModalIsOpen, setUploadMenuModalIsOpen] = useState(false); const [toastVisible, setToastVisible] = useState(null); + const [test, setTest] = useState(null); const handleNext = () => { navigate(routes.client.palmistryV1ScannedPhoto()); @@ -77,6 +78,13 @@ function Camera() { const getLines = async (file: File | Blob) => { setIsLoading(true); + + // Добавляем логирование размера файла + console.log('Размер файла перед отправкой:', { + bytesSize: file.size, + megabytesSize: (file.size / (1024 * 1024)).toFixed(2) + ' MB' + }); + const formData = new FormData(); formData.append("file", file); try { @@ -105,15 +113,38 @@ function Camera() { const onTakePhoto = async (photo: string) => { setUploadMenuModalIsOpen(false); - const file = DataURIToBlob(photo); - const result = await getLines(file); - dispatch( - actions.palmistry.update({ - photo, - }) - ); - if (!checkPalmistryLines(result?.lines || [])) return; - handleNext(); + + console.log('Начало обработки фото'); + + try { + console.log('Попытка конвертации base64 в Blob'); + const file = DataURIToBlob(photo); + + console.log('Размер Blob:', { + size: file.size, + megabytes: (file.size / (1024 * 1024)).toFixed(2) + ' MB' + }); + + const result = await getLines(file); + + // Очищаем большие переменные + URL.revokeObjectURL(URL.createObjectURL(file)); + + dispatch( + actions.palmistry.update({ + photo, + }) + ); + if (!checkPalmistryLines(result?.lines || [])) return; + handleNext(); + } catch (error) { + console.error('Ошибка при обработке фото:', error); + } finally { + // Принудительный запуск сборщика мусора (не гарантировано, но может помочь) + if (window.gc) { + window.gc(); + } + } }; const onSelectFile = async (event: React.ChangeEvent) => { @@ -155,10 +186,20 @@ function Camera() { Upload )} + {test &&
{test}
} {!isLoading && !uploadMenuModalIsOpen && ( - console.log("close")} + // onTakePhoto={onTakePhoto} + // /> + console.log("close")} onTakePhoto={onTakePhoto} + onError={(error) => { + console.error(error) + setTest(JSON.stringify(error)) + setToastVisible(EToastVisible.try_again) + }} /> )} {isLoading && ( diff --git a/src/components/palmistry/palm-camera-modal/palm-camera-modal.tsx b/src/components/palmistry/palm-camera-modal/palm-camera-modal.tsx index 4da6c0c..bb752e6 100644 --- a/src/components/palmistry/palm-camera-modal/palm-camera-modal.tsx +++ b/src/components/palmistry/palm-camera-modal/palm-camera-modal.tsx @@ -12,6 +12,28 @@ type Props = { export default function PalmCameraModal(props: Props) { const videoEl = React.useRef(null); + const [isVideoReady, setIsVideoReady] = React.useState(false); + + React.useEffect(() => { + const video = videoEl.current; + if (!video) return; + + const handleVideoReady = () => { + console.log('Метаданные видео загружены:', { + videoWidth: videoEl.current?.videoWidth, + videoHeight: videoEl.current?.videoHeight, + readyState: videoEl.current?.readyState + }); + setIsVideoReady(true); + video.play(); + }; + + video.addEventListener("loadedmetadata", handleVideoReady); + + return () => { + video.removeEventListener("loadedmetadata", handleVideoReady); + }; + }, []); const [mediaStream, setMediaStream] = React.useState( null @@ -31,17 +53,26 @@ export default function PalmCameraModal(props: Props) { video: { facingMode: { ideal: "environment" } }, }); + console.log('Медиа поток получен:', { + tracks: stream.getTracks().map(track => ({ + kind: track.kind, + settings: track.getSettings() + })) + }); + setMediaStream(stream); videoEl.current.srcObject = stream; - videoEl.current.addEventListener("loadedmetadata", videoEl.current.play); + // videoEl.current.addEventListener("loadedmetadata", videoEl.current.play); } catch (error) { console.error("Camera is not available", error); } }; const deactivateCamera = React.useCallback(() => { + if (!mediaStream) return; mediaStream?.getTracks().forEach((track) => track.stop()); + setMediaStream(null); }, [mediaStream]); React.useEffect(() => { @@ -51,29 +82,122 @@ export default function PalmCameraModal(props: Props) { React.useEffect(() => { return () => { deactivateCamera(); + if (videoEl.current) { + videoEl.current.srcObject = null; + videoEl.current.load(); + } }; }, [deactivateCamera]); const takePhoto = () => { + console.log('Начало захвата фото'); const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - if (!context || !videoEl.current) return null; + // Функция очистки ресурсов + const cleanup = () => { + // Очищаем canvas + if (context) { + context.clearRect(0, 0, canvas.width, canvas.height); + } + // Удаляем canvas + canvas.width = 0; + canvas.height = 0; + canvas.remove(); + }; - const width = videoEl.current.videoWidth; - const height = videoEl.current.videoHeight; + try { + if (!context || !videoEl.current) { + console.error('Контекст canvas или видео не доступны'); + return null; + }; - canvas.width = width; - canvas.height = height; - context.drawImage(videoEl.current, 0, 0, width, height); + const width = videoEl.current.videoWidth; + const height = videoEl.current.videoHeight; - const data = canvas.toDataURL("image/png"); + console.log('Размеры видео:', { + width, + height, + estimatedMemoryUsage: (width * height * 4 / (1024 * 1024)).toFixed(2) + ' MB' // 4 байта на пиксель + }); - return data; + console.log('Параметры видео перед захватом:', { + videoWidth: width, + videoHeight: height, + videoReady: isVideoReady, + videoCurrentTime: videoEl.current.currentTime, + readyState: videoEl.current.readyState + }); + + if (!width || !height) { + console.log("Video is not ready"); + return; + } + + // Попробуем уменьшить размер, если он слишком большой + // const MAX_DIMENSION = 1920; // максимальный размер стороны + // let targetWidth = width; + // let targetHeight = height; + + // if (width > MAX_DIMENSION || height > MAX_DIMENSION) { + // if (width > height) { + // targetWidth = MAX_DIMENSION; + // targetHeight = Math.round(height * (MAX_DIMENSION / width)); + // } else { + // targetHeight = MAX_DIMENSION; + // targetWidth = Math.round(width * (MAX_DIMENSION / height)); + // } + // } + + canvas.width = width; + canvas.height = height; + + console.log('Попытка отрисовки на canvas'); + context.drawImage(videoEl.current, 0, 0, width, height); + + console.log('Попытка конвертации в base64'); + const data = canvas.toDataURL("image/png"); + cleanup(); + + console.log('Размер base64:', { + length: data.length, + megabytes: (data.length / (1024 * 1024)).toFixed(2) + ' MB' + }); + + console.log('Результат захвата:', { + dataLength: data.length, + isEmptyCapture: data === 'data:,', + canvasWidth: canvas.width, + canvasHeight: canvas.height + }); + + if (data === "data:,") { + console.log("Couldn't get a photo"); + return; + } + return data; + } catch (error) { + console.error('Ошибка при обработке фото:', error); + cleanup(); + return null; + } }; - const onClickTakePhoto = () => { + const onClickTakePhoto = React.useCallback(() => { + console.log('Попытка сделать фото:', { + isVideoReady, + videoElement: { + width: videoEl.current?.videoWidth, + height: videoEl.current?.videoHeight, + readyState: videoEl.current?.readyState + } + }); + + if (!isVideoReady) { + console.warn('Видео не готово для захвата'); + return; + }; + const photo = takePhoto(); deactivateCamera(); @@ -81,7 +205,7 @@ export default function PalmCameraModal(props: Props) { if (!photo) return; props.onTakePhoto(photo); - }; + }, [isVideoReady]); return (