Add framer-motion and motion dependencies; implement animations in ModalContainer and PopupContainer components; enhance PopupWrapper for touch support; update SoundCheckModal and VoiceCheckModal for improved audio testing; refactor Select and ActionsPopover components for animation support.
This commit is contained in:
@@ -1,35 +1,44 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import ModalWrapper from "../ModalWrapper";
|
||||
import Button from "../ui/Button";
|
||||
import Select from "../ui/Select";
|
||||
import RestartIcon from "../icons/RestartIcon";
|
||||
import useModalStore from "../../store/modalStore";
|
||||
import SettingsModal from "./SettingsModal";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface VoiceCheckModalProps {
|
||||
selectedMicrophone: string;
|
||||
microphones: { deviceId: string; label: string }[];
|
||||
microphoneVolume: number;
|
||||
onSelectMicrophone: (label: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function VoiceCheckModal({
|
||||
selectedMicrophone,
|
||||
microphones,
|
||||
microphoneVolume,
|
||||
onSelectMicrophone,
|
||||
onClose,
|
||||
}: VoiceCheckModalProps) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const [audioLevel, setAudioLevel] = useState(0);
|
||||
const [status, setStatus] = useState<"default" | "success" | "error">(
|
||||
"default"
|
||||
);
|
||||
const [isTestRunning, setIsTestRunning] = useState(false);
|
||||
const [soundDetected, setSoundDetected] = useState(false);
|
||||
const [maxAudioLevel, setMaxAudioLevel] = useState(0);
|
||||
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const analyserRef = useRef<AnalyserNode | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
||||
const gainNodeRef = useRef<GainNode | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const testTimeoutRef = useRef<number | null>(null);
|
||||
const testTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const statusRef = useRef<"default" | "success" | "error">("default");
|
||||
const [isTestRunning, setIsTestRunning] = useState(false);
|
||||
|
||||
function detectAudioLevel() {
|
||||
if (!analyserRef.current) return;
|
||||
@@ -40,11 +49,16 @@ function VoiceCheckModal({
|
||||
const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
|
||||
setAudioLevel(average);
|
||||
|
||||
// Определяем статус на основе уровня звука
|
||||
// Обновляем максимальный уровень звука
|
||||
setMaxAudioLevel((prev) => Math.max(prev, average));
|
||||
|
||||
// Если звук обнаружен и статус ещё не success, устанавливаем success
|
||||
if (average > 10 && statusRef.current !== "success") {
|
||||
statusRef.current = "success";
|
||||
setStatus("success");
|
||||
if (average > 5) {
|
||||
if (statusRef.current !== "success") {
|
||||
statusRef.current = "success";
|
||||
setStatus("success");
|
||||
}
|
||||
setSoundDetected(true);
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(detectAudioLevel);
|
||||
@@ -72,13 +86,24 @@ function VoiceCheckModal({
|
||||
analyser.fftSize = 256;
|
||||
analyserRef.current = analyser;
|
||||
|
||||
const gainNode = audioContext.createGain();
|
||||
gainNode.gain.value = microphoneVolume / 100;
|
||||
gainNodeRef.current = gainNode;
|
||||
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
sourceRef.current = source;
|
||||
source.connect(analyser);
|
||||
|
||||
// Подключаем: source -> gainNode -> analyser -> destination
|
||||
source.connect(gainNode);
|
||||
gainNode.connect(analyser);
|
||||
analyser.connect(audioContext.destination);
|
||||
|
||||
// Сбрасываем статус при новом тесте
|
||||
statusRef.current = "default";
|
||||
setStatus("default");
|
||||
setSoundDetected(false);
|
||||
setAudioLevel(0);
|
||||
setMaxAudioLevel(0);
|
||||
setIsTestRunning(true);
|
||||
|
||||
// Запускаем проверку звука
|
||||
@@ -100,7 +125,6 @@ function VoiceCheckModal({
|
||||
// Если статус уже success, он остаётся success
|
||||
|
||||
setIsTestRunning(false);
|
||||
setAudioLevel(0);
|
||||
|
||||
// Очищаем ресурсы после завершения теста
|
||||
cleanupAudioResources();
|
||||
@@ -113,8 +137,8 @@ function VoiceCheckModal({
|
||||
}
|
||||
|
||||
function cleanupAudioResources() {
|
||||
// Отключаем source от analyser
|
||||
if (sourceRef.current && analyserRef.current) {
|
||||
// Отключаем source
|
||||
if (sourceRef.current) {
|
||||
try {
|
||||
sourceRef.current.disconnect();
|
||||
} catch {
|
||||
@@ -123,6 +147,26 @@ function VoiceCheckModal({
|
||||
sourceRef.current = null;
|
||||
}
|
||||
|
||||
// Отключаем gainNode
|
||||
if (gainNodeRef.current) {
|
||||
try {
|
||||
gainNodeRef.current.disconnect();
|
||||
} catch {
|
||||
// Ignore if already disconnected
|
||||
}
|
||||
gainNodeRef.current = null;
|
||||
}
|
||||
|
||||
// Отключаем analyser
|
||||
if (analyserRef.current) {
|
||||
try {
|
||||
analyserRef.current.disconnect();
|
||||
} catch {
|
||||
// Ignore if already disconnected
|
||||
}
|
||||
analyserRef.current = null;
|
||||
}
|
||||
|
||||
// Останавливаем все треки медиа-потока
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop());
|
||||
@@ -134,8 +178,6 @@ function VoiceCheckModal({
|
||||
audioContextRef.current.close();
|
||||
audioContextRef.current = null;
|
||||
}
|
||||
|
||||
analyserRef.current = null;
|
||||
}
|
||||
|
||||
function stopMicrophoneTest() {
|
||||
@@ -150,49 +192,48 @@ function VoiceCheckModal({
|
||||
|
||||
cleanupAudioResources();
|
||||
|
||||
setAudioLevel(0);
|
||||
// НЕ сбрасываем audioLevel и maxAudioLevel - они сохраняются до нового теста
|
||||
setSoundDetected(false);
|
||||
setIsTestRunning(false);
|
||||
}
|
||||
|
||||
function restartMicrophoneTest() {
|
||||
stopMicrophoneTest();
|
||||
// Небольшая задержка перед запуском нового теста
|
||||
setTimeout(() => {
|
||||
startMicrophoneTest();
|
||||
}, 100);
|
||||
setTimeout(startMicrophoneTest, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
startMicrophoneTest();
|
||||
|
||||
return () => {
|
||||
stopMicrophoneTest();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return stopMicrophoneTest;
|
||||
}, [selectedMicrophone]);
|
||||
|
||||
// Обновляем громкость микрофона при изменении слайдера
|
||||
useEffect(() => {
|
||||
if (gainNodeRef.current) {
|
||||
gainNodeRef.current.gain.value = microphoneVolume / 100;
|
||||
}
|
||||
}, [microphoneVolume]);
|
||||
|
||||
// Генерируем высоты для баров на основе уровня звука
|
||||
function generateBarHeights() {
|
||||
const baseHeights = [
|
||||
3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6,
|
||||
];
|
||||
const multiplier = Math.min(audioLevel / 50, 1.5);
|
||||
// Используем maxAudioLevel для сохранения максимальной высоты
|
||||
const levelToUse = isTestRunning ? audioLevel : maxAudioLevel;
|
||||
const multiplier = Math.min(levelToUse / 5);
|
||||
return baseHeights.map((h) => Math.max(3, Math.min(60, h * multiplier)));
|
||||
}
|
||||
|
||||
const barHeights = generateBarHeights();
|
||||
|
||||
return (
|
||||
<ModalWrapper title="" className="max-w-[21.111vw]">
|
||||
<div className="flex flex-col 2xl:gap-[1.667vw] gap-6 2xl:py-[1.667vw] py-6">
|
||||
{/* Заголовок и описание */}
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2 text-center">
|
||||
<h2 className="title-l font-medium">Говорите</h2>
|
||||
<p className="text-s text-[#7D7D7D]">
|
||||
Вы должны видеть движение на индикаторе
|
||||
</p>
|
||||
</div>
|
||||
console.log(barHeights);
|
||||
|
||||
return (
|
||||
<ModalWrapper className="2xl:max-w-[21.111vw] max-w-[304px]">
|
||||
<div className="flex flex-col 2xl:gap-[1.667vw] gap-6 2xl:py-[1.667vw] py-6">
|
||||
{/* Выбор микрофона */}
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.278vw] gap-1">
|
||||
<p className="caption-xs text-[#7D7D7D] font-medium">Микрофон</p>
|
||||
@@ -205,12 +246,15 @@ function VoiceCheckModal({
|
||||
|
||||
{/* Визуализация уровня звука */}
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2">
|
||||
<div className="flex items-end justify-center 2xl:gap-[0.208vw] gap-[3px] 2xl:h-[4.167vw] h-[60px]">
|
||||
<div className="flex items-center justify-center 2xl:gap-[0.208vw] gap-[3px] 2xl:h-[4.167vw] h-[60px]">
|
||||
{barHeights.map((height, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="2xl:w-[0.208vw] w-[3px] bg-[#D6D6D6] 2xl:rounded-[0.556vw] rounded-lg transition-all duration-100"
|
||||
style={{ height }}
|
||||
className={clsx(
|
||||
`2xl:rounded-[0.556vw] rounded-lg transition-all duration-150 2xl:w-[0.208vw] w-[3px]`,
|
||||
soundDetected ? "bg-[#7B60F3]" : "bg-[#D6D6D6]"
|
||||
)}
|
||||
style={{ height: `${(height / 1440) * innerWidth}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -227,9 +271,14 @@ function VoiceCheckModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Кнопки */}
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2 text-center">
|
||||
<h2 className="title-l font-medium">Говорите</h2>
|
||||
<p className="text-s text-[#7D7D7D]">
|
||||
Вы должны видеть движение на индикаторе
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col 2xl:gap-[0.556vw] gap-2">
|
||||
{/* Кнопка повторной проверки */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
@@ -243,11 +292,10 @@ function VoiceCheckModal({
|
||||
Повторить проверку
|
||||
</Button>
|
||||
|
||||
{/* Кнопка завершения */}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
onClick={onClose}
|
||||
onClick={() => setModal(<SettingsModal />)}
|
||||
className="w-full"
|
||||
>
|
||||
Завершить
|
||||
|
||||
Reference in New Issue
Block a user