Update API URL to localhost in .env; add equalizer_bars.svg for audio visualization; refactor PopupWrapper and SessionUsersPanel for improved responsiveness; enhance SoundCheckModal and VoiceCheckModal with audio playback and visualization features; update SharePopup to handle sharing functionality; improve UI components for better interaction and consistency.
This commit is contained in:
@@ -44,11 +44,11 @@ function PopupWrapper({
|
||||
setPosition({
|
||||
x: Math.min(
|
||||
Math.max(0, position.x + x - mouseDownPosition.x),
|
||||
window.innerWidth - wrapperRef.current.clientWidth
|
||||
innerWidth - wrapperRef.current.clientWidth
|
||||
),
|
||||
y: Math.min(
|
||||
Math.max(0, position.y + y - mouseDownPosition.y),
|
||||
window.innerHeight - wrapperRef.current.clientHeight
|
||||
innerHeight - wrapperRef.current.clientHeight
|
||||
),
|
||||
});
|
||||
setMouseDownPosition({ x, y });
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function SessionUsersPanel() {
|
||||
transform: `translate(${isLeft ? "0" : "-100%"}, ${
|
||||
isTop ? "0" : "-100%"
|
||||
})`,
|
||||
transition: "all 0.3s ease-out",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import ModalWrapper from "../ModalWrapper";
|
||||
import Button from "../ui/Button";
|
||||
import RestartIcon from "../icons/RestartIcon";
|
||||
@@ -21,10 +21,46 @@ function SoundCheckModal({
|
||||
}: SoundCheckModalProps) {
|
||||
const { setModal } = useModalStore();
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [playProgress, setPlayProgress] = useState(0); // Прогресс воспроизведения 0-1
|
||||
const playStartTimeRef = useRef<number>(0);
|
||||
|
||||
// Высоты баров эквалайзера (16 баров как в дизайне)
|
||||
const barCount = 16;
|
||||
const barHeights = [
|
||||
3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6,
|
||||
];
|
||||
|
||||
// Обновление прогресса воспроизведения
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
|
||||
const updateProgress = () => {
|
||||
const elapsed = Date.now() - playStartTimeRef.current;
|
||||
const progress = Math.min(elapsed / 3000, 1); // 3 секунды
|
||||
setPlayProgress(progress);
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameRef.current = requestAnimationFrame(updateProgress);
|
||||
}
|
||||
};
|
||||
|
||||
updateProgress();
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [isPlaying]);
|
||||
|
||||
// Очистка AudioContext при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
if (audioContextRef.current) {
|
||||
audioContextRef.current.close();
|
||||
audioContextRef.current = null;
|
||||
@@ -42,6 +78,7 @@ function SoundCheckModal({
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
// Подключаем напрямую
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
@@ -54,12 +91,23 @@ function SoundCheckModal({
|
||||
gainNode.gain.setValueAtTime(baseVolume * 0.3, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.01,
|
||||
audioContext.currentTime + 1
|
||||
audioContext.currentTime + 3
|
||||
);
|
||||
|
||||
// Играем звук 1 секунду
|
||||
// Запускаем анимацию прогресса
|
||||
playStartTimeRef.current = Date.now();
|
||||
setIsPlaying(true);
|
||||
setPlayProgress(0);
|
||||
|
||||
// Играем звук 3 секунды
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 1);
|
||||
oscillator.stop(audioContext.currentTime + 3);
|
||||
|
||||
// Останавливаем анимацию после окончания звука
|
||||
setTimeout(() => {
|
||||
setIsPlaying(false);
|
||||
setPlayProgress(0);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -76,20 +124,28 @@ function SoundCheckModal({
|
||||
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.833vw] gap-3">
|
||||
<div className="flex items-center justify-center 2xl:gap-[0.208vw] gap-[3px] 2xl:h-[4.167vw] h-[60px]">
|
||||
{[3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6].map(
|
||||
(height, index) => (
|
||||
{barHeights.map((height, index) => {
|
||||
// Определяем, заполнен ли бар синим (прогресс слева направо)
|
||||
const barProgress = (index + 1) / barCount;
|
||||
const isActivated = playProgress >= barProgress || !isPlaying;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="2xl:w-[0.208vw] w-[3px] bg-[#D6D6D6] 2xl:rounded-[0.556vw] rounded-lg"
|
||||
style={{ height: `${(height / 1440) * innerWidth}px` }}
|
||||
className="2xl:w-[0.208vw] w-[3px] 2xl:rounded-[0.556vw] rounded-lg transition-all duration-150 ease-out"
|
||||
style={{
|
||||
height: `${height}px`,
|
||||
backgroundColor: isActivated ? "#7B60F3" : "#D6D6D6",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={playTestSound}
|
||||
className="2xl:size-[2.778vw] size-10 rounded-full bg-[#7B60F3] hover:bg-[#9184F6] active:bg-[#B3AAF9] flex items-center justify-center text-white transition-colors"
|
||||
disabled={isPlaying}
|
||||
className="2xl:size-[2.778vw] size-10 rounded-full bg-[#7B60F3] hover:bg-[#9184F6] active:bg-[#B3AAF9] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center text-white transition-colors"
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<RestartIcon />
|
||||
|
||||
@@ -24,13 +24,11 @@ function VoiceCheckModal({
|
||||
}: 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);
|
||||
@@ -41,30 +39,6 @@ function VoiceCheckModal({
|
||||
const testTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const statusRef = useRef<"default" | "success" | "error">("default");
|
||||
|
||||
function detectAudioLevel() {
|
||||
if (!analyserRef.current) return;
|
||||
|
||||
const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);
|
||||
analyserRef.current.getByteFrequencyData(dataArray);
|
||||
|
||||
const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
|
||||
setAudioLevel(average);
|
||||
|
||||
// Обновляем максимальный уровень звука
|
||||
setMaxAudioLevel((prev) => Math.max(prev, average));
|
||||
|
||||
// Если звук обнаружен и статус ещё не success, устанавливаем success
|
||||
if (average > 5) {
|
||||
if (statusRef.current !== "success") {
|
||||
statusRef.current = "success";
|
||||
setStatus("success");
|
||||
}
|
||||
setSoundDetected(true);
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(detectAudioLevel);
|
||||
}
|
||||
|
||||
async function startMicrophoneTest() {
|
||||
// Проверяем доступность API
|
||||
if (!isMediaDevicesSupported()) {
|
||||
@@ -94,7 +68,10 @@ function VoiceCheckModal({
|
||||
audioContextRef.current = audioContext;
|
||||
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
analyser.fftSize = 2048; // Увеличиваем для более точного анализа
|
||||
analyser.smoothingTimeConstant = 0.85; // Увеличиваем сглаживание для плавных волн
|
||||
analyser.minDecibels = -90; // Минимальный уровень для лучшей чувствительности
|
||||
analyser.maxDecibels = -10; // Максимальный уровень
|
||||
analyserRef.current = analyser;
|
||||
|
||||
const gainNode = audioContext.createGain();
|
||||
@@ -113,21 +90,10 @@ function VoiceCheckModal({
|
||||
statusRef.current = "default";
|
||||
setStatus("default");
|
||||
setSoundDetected(false);
|
||||
setAudioLevel(0);
|
||||
setMaxAudioLevel(0);
|
||||
setIsTestRunning(true);
|
||||
|
||||
// Запускаем проверку звука
|
||||
detectAudioLevel();
|
||||
|
||||
// Останавливаем проверку через 3 секунды и устанавливаем результат
|
||||
testTimeoutRef.current = setTimeout(() => {
|
||||
// Останавливаем анимацию
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = null;
|
||||
}
|
||||
|
||||
// Устанавливаем финальный статус
|
||||
if (statusRef.current === "default") {
|
||||
statusRef.current = "error";
|
||||
@@ -203,7 +169,6 @@ function VoiceCheckModal({
|
||||
|
||||
cleanupAudioResources();
|
||||
|
||||
// НЕ сбрасываем audioLevel и maxAudioLevel - они сохраняются до нового теста
|
||||
setSoundDetected(false);
|
||||
setIsTestRunning(false);
|
||||
}
|
||||
@@ -227,20 +192,94 @@ function VoiceCheckModal({
|
||||
}
|
||||
}, [microphoneVolume]);
|
||||
|
||||
// Генерируем высоты для баров на основе уровня звука
|
||||
function generateBarHeights() {
|
||||
const baseHeights = [
|
||||
3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6,
|
||||
];
|
||||
// Используем maxAudioLevel для сохранения максимальной высоты
|
||||
const levelToUse = isTestRunning ? audioLevel : maxAudioLevel;
|
||||
const multiplier = Math.min(levelToUse / 5);
|
||||
return baseHeights.map((h) => Math.max(3, Math.min(60, h * multiplier)));
|
||||
}
|
||||
// Состояние для баров эквалайзера (40 баров для плавных волн)
|
||||
const barCount = 40;
|
||||
const [barHeights, setBarHeights] = useState<number[]>(
|
||||
new Array(barCount).fill(3)
|
||||
);
|
||||
|
||||
const barHeights = generateBarHeights();
|
||||
// Обновляем бары эквалайзера в режиме реального времени
|
||||
useEffect(() => {
|
||||
if (!isTestRunning || !analyserRef.current) return;
|
||||
|
||||
console.log(barHeights);
|
||||
const updateBars = () => {
|
||||
if (!analyserRef.current) return;
|
||||
|
||||
const bufferLength = analyserRef.current.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
analyserRef.current.getByteFrequencyData(dataArray);
|
||||
|
||||
// Сначала создаём меньше опорных точек для интерполяции
|
||||
const keyPoints = Math.floor(barCount / 2.5); // ~16 опорных точек
|
||||
const keyHeights: number[] = [];
|
||||
|
||||
for (let i = 0; i < keyPoints; i++) {
|
||||
// Логарифмическое распределение частот
|
||||
const startFreq = Math.pow(2, (i / keyPoints) * 10);
|
||||
const endFreq = Math.pow(2, ((i + 1) / keyPoints) * 10);
|
||||
|
||||
const startIndex = Math.floor((startFreq / 1024) * bufferLength);
|
||||
const endIndex = Math.floor((endFreq / 1024) * bufferLength);
|
||||
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let j = startIndex; j < endIndex && j < bufferLength; j++) {
|
||||
sum += dataArray[j];
|
||||
count++;
|
||||
}
|
||||
|
||||
const average = count > 0 ? sum / count : 0;
|
||||
// Нормализуем к диапазону 3-60 пикселей
|
||||
const normalizedHeight = Math.min(
|
||||
60,
|
||||
Math.max(3, (average / 255) * 60 * 2.5)
|
||||
);
|
||||
keyHeights.push(normalizedHeight);
|
||||
}
|
||||
|
||||
// Интерполируем между опорными точками для плавных волн
|
||||
const newBarHeights: number[] = [];
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const position = (i / barCount) * (keyPoints - 1);
|
||||
const index = Math.floor(position);
|
||||
const fraction = position - index;
|
||||
|
||||
if (index >= keyPoints - 1) {
|
||||
newBarHeights.push(keyHeights[keyPoints - 1]);
|
||||
} else {
|
||||
// Линейная интерполяция между двумя опорными точками
|
||||
const interpolated =
|
||||
keyHeights[index] * (1 - fraction) +
|
||||
keyHeights[index + 1] * fraction;
|
||||
newBarHeights.push(interpolated);
|
||||
}
|
||||
}
|
||||
|
||||
setBarHeights(newBarHeights);
|
||||
|
||||
// Определяем наличие звука по максимальному бару
|
||||
const maxBar = Math.max(...newBarHeights);
|
||||
if (maxBar > 10) {
|
||||
// Если есть бары выше 10px, значит есть звук
|
||||
if (statusRef.current !== "success") {
|
||||
statusRef.current = "success";
|
||||
setStatus("success");
|
||||
}
|
||||
setSoundDetected(true);
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(updateBars);
|
||||
};
|
||||
|
||||
updateBars();
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [isTestRunning, barCount]);
|
||||
|
||||
return (
|
||||
<ModalWrapper className="2xl:max-w-[21.111vw] max-w-[304px]">
|
||||
@@ -255,31 +294,40 @@ function VoiceCheckModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Визуализация уровня звука */}
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2">
|
||||
<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={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 className="flex flex-col 2xl:gap-[0.833vw] gap-3">
|
||||
{/* Визуализация эквалайзера */}
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2">
|
||||
<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={clsx(
|
||||
"2xl:w-[0.208vw] w-[3px] 2xl:rounded-[0.556vw] rounded-lg transition-all duration-150 ease-out",
|
||||
soundDetected ? "bg-[#7B60F3]" : "bg-[#D6D6D6]"
|
||||
)}
|
||||
style={{
|
||||
height: `${Math.max(3, height)}px`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Статус */}
|
||||
{!isTestRunning && status === "success" && (
|
||||
<p className="caption-s text-[#29AF61] font-medium">Звук есть!</p>
|
||||
)}
|
||||
{!isTestRunning && status === "error" && (
|
||||
<p className="caption-s text-[#FF4517] font-medium">
|
||||
Звук не обнаружен
|
||||
</p>
|
||||
)}
|
||||
{isTestRunning && (
|
||||
<p className="caption-s text-[#7D7D7D] font-medium">
|
||||
Проверка...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!isTestRunning && status === "success" && (
|
||||
<p className="caption-s text-[#29AF61] font-medium">Звук есть!</p>
|
||||
)}
|
||||
{!isTestRunning && status === "error" && (
|
||||
<p className="caption-s text-[#FF4517] font-medium">
|
||||
Звук не обнаружен
|
||||
</p>
|
||||
)}
|
||||
{isTestRunning && (
|
||||
<p className="caption-s text-[#7D7D7D] font-medium">Проверка...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center 2xl:gap-[0.556vw] gap-2 text-center">
|
||||
|
||||
@@ -14,6 +14,7 @@ function QRCodePopup({ link }: QRCodePopupProps) {
|
||||
|
||||
return (
|
||||
<PopupWrapper
|
||||
className="w-[21.667vw]"
|
||||
draggable
|
||||
leftButton={
|
||||
<Button
|
||||
|
||||
@@ -9,11 +9,23 @@ import QRCodePopup from "./QRCodePopup";
|
||||
function SharePopup({ link }: { link: string }) {
|
||||
const { setPopup } = usePopupStore();
|
||||
|
||||
function handleShare() {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: "Пригласить",
|
||||
text: "Присоединяйся к моей встрече",
|
||||
url: link,
|
||||
});
|
||||
} else {
|
||||
navigator.clipboard.writeText(link);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupWrapper
|
||||
title="Пригласить"
|
||||
draggable
|
||||
className="max-w-[21.667vw]"
|
||||
className="w-[21.667vw]"
|
||||
leftButton={
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -30,7 +42,12 @@ function SharePopup({ link }: { link: string }) {
|
||||
<p className="title-s mb-[0.833vw] font-medium">Скопировать ссылку</p>
|
||||
<LinkShare link={link} />
|
||||
</div>
|
||||
<Button variant="primary" size="large" className="w-full">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="w-full"
|
||||
onClick={handleShare}
|
||||
>
|
||||
Отправить
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<ShareFilledIcon />
|
||||
|
||||
@@ -29,7 +29,7 @@ function Button({
|
||||
onClick?.(e);
|
||||
}}
|
||||
className={clsx(
|
||||
"transition-all select-none cursor-pointer disabled:!cursor-default flex outline-none gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:!text-[#D6D6D6]",
|
||||
"transition-colors select-none cursor-pointer disabled:!cursor-default flex outline-none gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:!text-[#D6D6D6]",
|
||||
isActive && "bg-[#F3F1FD] !text-[#7B60F3]",
|
||||
variant === "menu" &&
|
||||
"text-[#7D7D7D] hover:bg-[#F3F3F3] active:bg-[#F3F1FD] active:text-[#7B60F3]",
|
||||
|
||||
@@ -4,12 +4,10 @@ interface ControlButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size: "small" | "large";
|
||||
icon: React.ReactNode;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
function ControlButton({
|
||||
size,
|
||||
enabled,
|
||||
icon,
|
||||
className,
|
||||
onClick,
|
||||
@@ -20,13 +18,10 @@ function ControlButton({
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
className={clsx(
|
||||
"backdrop-blur-[10px] rounded-full transition-all cursor-pointer disabled:!cursor-default outline-none",
|
||||
size === "large" ? "2xl:p-[0.833vw] p-3" : "2xl:p-[0.417vw] p-[6px]",
|
||||
!enabled
|
||||
? "bg-[#FF4517] hover:bg-[#FF4517]/85"
|
||||
: size === "large"
|
||||
? "bg-[#FFFFFF]/15 hover:bg-[#FFFFFF]/25"
|
||||
: "bg-[#141414]/15 hover:bg-[#141414]/25",
|
||||
"backdrop-blur-[10px] rounded-full transition-colors cursor-pointer disabled:!cursor-default outline-none disabled:bg-[#FF4517] disabled:hover:bg-[#FF4517]/85",
|
||||
size === "large"
|
||||
? "2xl:p-[0.833vw] p-3 enabled:bg-[#FFFFFF]/15 enabled:hover:bg-[#FFFFFF]/25"
|
||||
: "2xl:p-[0.417vw] p-[6px] enabled:bg-[#141414]/15 enabled:hover:bg-[#141414]/25",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -97,7 +97,9 @@ function Select({
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
<span className="text-s">{option}</span>
|
||||
<span className="text-s line-clamp-1 text-ellipsis text-nowrap">
|
||||
{option}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import HandRaisedOffFilledIcon from "../icons/HandRaisedOffFilledIcon";
|
||||
import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon";
|
||||
import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon";
|
||||
@@ -23,7 +21,7 @@ interface UserCameraControlsProps {
|
||||
interface UserCameraProps extends UserCameraControlsProps {
|
||||
isAdmin?: boolean;
|
||||
name?: string;
|
||||
mediaStream?: string;
|
||||
mediaStream?: MediaStream | null;
|
||||
isSpeaking?: boolean;
|
||||
}
|
||||
|
||||
@@ -37,23 +35,25 @@ export default function UserCamera({
|
||||
isSpeaking = false,
|
||||
isAdmin = false,
|
||||
name = "Гость",
|
||||
mediaStream = "",
|
||||
mediaStream = null,
|
||||
}: UserCameraProps) {
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.srcObject = mediaStream;
|
||||
}
|
||||
}, [mediaStream]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
animate={{
|
||||
width: isHover ? "10.833vw" : "6.944vw",
|
||||
border: isSpeaking
|
||||
? "0.139vw solid #7B60F3"
|
||||
: "0.139vw solid #FFFFFF4D",
|
||||
}}
|
||||
<div
|
||||
className={clsx(
|
||||
"aspect-square group rounded-[1.667vw] bg-yellow-500 relative flex-shrink-0 transition-all duration-300 pointer-events-auto hover:w-[10.833vw]w-[6.944vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1),0_2px_2px_0_rgba(0,0,0,0.06)]",
|
||||
isAdmin && "order-3"
|
||||
"aspect-square group 2xl:rounded-[1.667vw] rounded-2xl relative flex-shrink-0 transition-[width,box-shadow,background-color] duration-300 pointer-events-auto hover:w-[10.833vw] w-[6.944vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1),0_2px_2px_0_rgba(0,0,0,0.06)]",
|
||||
isAdmin && "order-3",
|
||||
isSpeaking
|
||||
? "ring-[0.139vw] ring-[#7B60F3]"
|
||||
: "ring-[0.069vw] ring-[#FFFFFF4D]",
|
||||
isVideoOff ? "bg-green-500" : "bg-yellow-500"
|
||||
)}
|
||||
>
|
||||
{isAdmin && <Admin className="absolute top-0 right-0" />}
|
||||
@@ -66,7 +66,7 @@ export default function UserCamera({
|
||||
</div>
|
||||
|
||||
<video
|
||||
src={mediaStream}
|
||||
ref={ref}
|
||||
className="size-full object-cover"
|
||||
autoPlay
|
||||
muted={isMuted}
|
||||
@@ -77,12 +77,11 @@ export default function UserCamera({
|
||||
isMuted={isMuted}
|
||||
isVideoOff={isVideoOff}
|
||||
isControlDisabled={isControlDisabled}
|
||||
isHover={isHover}
|
||||
onMute={onMute}
|
||||
onVideoOff={onVideoOff}
|
||||
onCanControl={onCanControl}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,63 +89,51 @@ function UserCameraControls({
|
||||
isMuted,
|
||||
isVideoOff,
|
||||
isControlDisabled,
|
||||
isHover,
|
||||
onMute,
|
||||
onVideoOff,
|
||||
onCanControl,
|
||||
}: UserCameraControlsProps & { isHover: boolean }) {
|
||||
}: UserCameraControlsProps) {
|
||||
return (
|
||||
<div className="absolute bottom-[0.278vw] left-1/2 translate-x-[-50%]">
|
||||
<AnimatePresence mode="wait">
|
||||
{isHover ? (
|
||||
<motion.div
|
||||
key="controls"
|
||||
className="flex gap-[0.278vw] mb-[0.278vw]"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ControlButton
|
||||
icon={isMuted ? <MicrophoneOffIcon /> : <MicrophoneFilledIcon />}
|
||||
size={"small"}
|
||||
enabled={!isMuted}
|
||||
onClick={onMute}
|
||||
/>
|
||||
<ControlButton
|
||||
icon={isVideoOff ? <VideoOffFilledIcon /> : <VideoFilledIcon />}
|
||||
size={"small"}
|
||||
enabled={!isVideoOff}
|
||||
onClick={onVideoOff}
|
||||
/>
|
||||
<ControlButton
|
||||
icon={
|
||||
isControlDisabled ? (
|
||||
<HandRaisedOffFilledIcon />
|
||||
) : (
|
||||
<HandRaisedFilledIcon />
|
||||
)
|
||||
}
|
||||
size={"small"}
|
||||
enabled={!isControlDisabled}
|
||||
onClick={onCanControl}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="controls-muted"
|
||||
className="size-[1.667vw] bg-[#14141426] backdrop-blur-[4px] rounded-full flex items-center justify-center z-10"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isMuted ? 1 : 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="size-[0.972vw] text-white">
|
||||
<MicrophoneOffIcon />
|
||||
</div>
|
||||
</motion.div>
|
||||
<div className="absolute transition-[bottom] duration-300 2xl:bottom-[0.278vw] 2xl:group-hover:bottom-[0.556vw] group-hover:bottom-2 bottom-1 left-1/2 -translate-x-1/2">
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:size-[1.667vw] size-6 bg-[#14141426] backdrop-blur-[4px] transition-opacity duration-300 rounded-full flex items-center justify-center z-10a absolute left-1/2 -translate-x-1/2 2xl:bottom-0 [0.278vw] group-hover:opacity-0",
|
||||
isMuted ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
</AnimatePresence>
|
||||
>
|
||||
<div className="size-[0.972vw] text-white">
|
||||
<MicrophoneOffIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex gap-[0.278vw] mb-[0.278vw] group-hover:opacity-100 opacity-0 transition-opacity duration-300"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ControlButton
|
||||
icon={isMuted ? <MicrophoneOffIcon /> : <MicrophoneFilledIcon />}
|
||||
size={"small"}
|
||||
disabled={isMuted}
|
||||
onClick={onMute}
|
||||
/>
|
||||
<ControlButton
|
||||
icon={isVideoOff ? <VideoOffFilledIcon /> : <VideoFilledIcon />}
|
||||
size={"small"}
|
||||
disabled={isVideoOff}
|
||||
onClick={onVideoOff}
|
||||
/>
|
||||
<ControlButton
|
||||
icon={
|
||||
isControlDisabled ? (
|
||||
<HandRaisedOffFilledIcon />
|
||||
) : (
|
||||
<HandRaisedFilledIcon />
|
||||
)
|
||||
}
|
||||
size={"small"}
|
||||
disabled={isControlDisabled}
|
||||
onClick={onCanControl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,29 +23,29 @@ export default function UserDevicesControls() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hidden order-4 2xl:grid grid-cols-2 gap-[0.278vw] aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto">
|
||||
<div className="hidden order-4 2xl:grid grid-cols-2 gap-[0.278vw] aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto">
|
||||
<ControlButton
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
size="large"
|
||||
icon={<MicrophoneFilledIcon />}
|
||||
onClick={ToggleAudioDevice}
|
||||
enabled={true}
|
||||
/>
|
||||
<ControlButton
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
size="large"
|
||||
icon={<VideoFilledIcon />}
|
||||
enabled={true}
|
||||
onClick={ToggleVideoDevice}
|
||||
/>
|
||||
<ControlButton
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
size="large"
|
||||
icon={<HandRaisedFilledIcon />}
|
||||
onClick={ToggleCanControl}
|
||||
enabled={true}
|
||||
/>
|
||||
<ControlButton
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
size="large"
|
||||
icon={<CogFilledIcon />}
|
||||
enabled={true}
|
||||
onClick={ToggleSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@ import usePopupStore from "../store/popupStore";
|
||||
import SettingsModal from "../components/modals/SettingsModal";
|
||||
import useModalStore from "../store/modalStore";
|
||||
import CogFilledIcon from "../components/icons/CogFilledIcon";
|
||||
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
|
||||
import SessionUsersPanel from "../components/SessionUsersPanel";
|
||||
import SharePopup from "../components/popups/SharePopup";
|
||||
|
||||
function HomePage() {
|
||||
const { data: user } = useMe();
|
||||
@@ -33,7 +33,9 @@ function HomePage() {
|
||||
|
||||
<FloatingActionButton
|
||||
variant="default"
|
||||
onClick={() => setPopup(<ParticipantsPopup />)}
|
||||
onClick={() =>
|
||||
setPopup(<SharePopup link="https://estate.stream/ahdy12jdco1" />)
|
||||
}
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||||
<ShareFilledIcon />
|
||||
|
||||
Reference in New Issue
Block a user