Refactor SessionUsersPanel to improve drag-and-drop functionality with enhanced state management and animations. Update UserCamera and UserDevicesControls for consistent styling and layout adjustments. Modify NewSessionPage to import updated SessionUsersPanel component.
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface Position {
|
||||
top?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
}
|
||||
|
||||
interface DraggableContainerProps {
|
||||
/** Содержимое контейнера */
|
||||
children: ReactNode;
|
||||
/** Включить снэпинг к ближайшей четверти экрана при отпускании (по умолчанию true) */
|
||||
enableSnapping?: boolean;
|
||||
/** Автоматическое flex-выравнивание в зависимости от прижатого угла (по умолчанию false) */
|
||||
autoAlign?: boolean;
|
||||
/** Начальная позиция контейнера */
|
||||
initialPosition?: Position;
|
||||
/** Отступ от краев экрана при снэпинге (по умолчанию 20px) */
|
||||
padding?: number;
|
||||
/** Дополнительные CSS классы */
|
||||
className?: string;
|
||||
/** Колбэк при изменении позиции */
|
||||
onPositionChange?: (position: Position) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draggable Container - перетаскиваемый контейнер с опциональным снэпингом
|
||||
*
|
||||
* Логика снэпинга:
|
||||
* - Экран делится на 4 части (пополам по ширине и высоте)
|
||||
* - При отпускании определяется в какой четверти находится центр контейнера
|
||||
* - Контейнер прилипает к соответствующему углу с использованием top/bottom + left/right
|
||||
*
|
||||
* @example
|
||||
* // Базовое использование с автоматическим выравниванием
|
||||
* <DraggableContainer enableSnapping={true} autoAlign={true}>
|
||||
* <YourContent />
|
||||
* </DraggableContainer>
|
||||
*
|
||||
* @example
|
||||
* // Без автоматического выравнивания (управляется вручную через className)
|
||||
* <DraggableContainer enableSnapping={true} className="flex flex-col gap-4">
|
||||
* <YourContent />
|
||||
* </DraggableContainer>
|
||||
*/
|
||||
export default function DraggableContainer({
|
||||
children,
|
||||
enableSnapping = false,
|
||||
autoAlign = false,
|
||||
initialPosition = { top: 20, right: 20 },
|
||||
padding = 20,
|
||||
className = "",
|
||||
onPositionChange,
|
||||
}: DraggableContainerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef({
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
initialPosition: { top: 0, left: 0 },
|
||||
});
|
||||
|
||||
const [position, setPosition] = useState<Position>(initialPosition);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragStartAlignment, setDragStartAlignment] = useState<string>("");
|
||||
|
||||
const getAlignmentClassesFromPosition = (pos: Position): string => {
|
||||
if (!autoAlign) return "";
|
||||
|
||||
const vertical = pos.bottom !== undefined ? "items-end" : "items-start";
|
||||
const horizontal =
|
||||
pos.right !== undefined ? "justify-end" : "justify-start";
|
||||
|
||||
return `${vertical} ${horizontal}`;
|
||||
};
|
||||
|
||||
const snapToQuadrant = (x: number, y: number): Position => {
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const containerWidth = containerRef.current?.offsetWidth || 0;
|
||||
const containerHeight = containerRef.current?.offsetHeight || 0;
|
||||
|
||||
// Делим экран пополам по ширине и высоте
|
||||
const halfWidth = windowWidth / 2;
|
||||
const halfHeight = windowHeight / 2;
|
||||
|
||||
// Определяем центр контейнера
|
||||
const centerX = x + containerWidth / 2;
|
||||
const centerY = y + containerHeight / 2;
|
||||
|
||||
// Определяем в какой четверти находится центр контейнера
|
||||
const isLeft = centerX < halfWidth;
|
||||
const isTop = centerY < halfHeight;
|
||||
|
||||
// Возвращаем позицию в зависимости от четверти
|
||||
if (isTop && isLeft) {
|
||||
// Верхняя левая четверть
|
||||
return { top: padding, left: padding };
|
||||
} else if (isTop && !isLeft) {
|
||||
// Верхняя правая четверть
|
||||
return { top: padding, right: padding };
|
||||
} else if (!isTop && isLeft) {
|
||||
// Нижняя левая четверть
|
||||
return { bottom: padding, left: padding };
|
||||
} else {
|
||||
// Нижняя правая четверть
|
||||
return { bottom: padding, right: padding };
|
||||
}
|
||||
};
|
||||
|
||||
const startDrag = (clientX: number, clientY: number) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
// Сохраняем текущие классы выравнивания перед началом драга
|
||||
if (autoAlign) {
|
||||
setDragStartAlignment(getAlignmentClassesFromPosition(position));
|
||||
}
|
||||
|
||||
dragRef.current = {
|
||||
isDragging: true,
|
||||
startX: clientX,
|
||||
startY: clientY,
|
||||
initialPosition: {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
},
|
||||
};
|
||||
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
startDrag(e.clientX, e.clientY);
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
if (e.touches.length > 0) {
|
||||
startDrag(e.touches[0].clientX, e.touches[0].clientY);
|
||||
}
|
||||
};
|
||||
|
||||
const updateDragPosition = (clientX: number, clientY: number) => {
|
||||
if (!dragRef.current.isDragging) return;
|
||||
|
||||
const deltaX = clientX - dragRef.current.startX;
|
||||
const deltaY = clientY - dragRef.current.startY;
|
||||
|
||||
const newTop = dragRef.current.initialPosition.top + deltaY;
|
||||
const newLeft = dragRef.current.initialPosition.left + deltaX;
|
||||
|
||||
// Во время перетаскивания используем только top и left
|
||||
const newPosition = {
|
||||
top: newTop,
|
||||
left: newLeft,
|
||||
};
|
||||
|
||||
setPosition(newPosition);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
updateDragPosition(e.clientX, e.clientY);
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
if (e.touches.length > 0) {
|
||||
e.preventDefault(); // Предотвращаем скролл на мобильных
|
||||
updateDragPosition(e.touches[0].clientX, e.touches[0].clientY);
|
||||
}
|
||||
};
|
||||
|
||||
const endDrag = () => {
|
||||
if (!dragRef.current.isDragging || !containerRef.current) return;
|
||||
|
||||
dragRef.current.isDragging = false;
|
||||
setIsDragging(false);
|
||||
setDragStartAlignment(""); // Очищаем сохраненное выравнивание
|
||||
|
||||
if (enableSnapping) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const snappedPosition = snapToQuadrant(rect.left, rect.top);
|
||||
|
||||
// Конвертируем текущую позицию в те же свойства, что будут в финальной
|
||||
// чтобы transition работал правильно
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
const intermediatePosition: Position = {};
|
||||
|
||||
// Определяем какие свойства будут в финальной позиции и устанавливаем текущие значения
|
||||
if (snappedPosition.top !== undefined) {
|
||||
intermediatePosition.top = rect.top;
|
||||
}
|
||||
if (snappedPosition.bottom !== undefined) {
|
||||
intermediatePosition.bottom = windowHeight - rect.bottom;
|
||||
}
|
||||
if (snappedPosition.left !== undefined) {
|
||||
intermediatePosition.left = rect.left;
|
||||
}
|
||||
if (snappedPosition.right !== undefined) {
|
||||
intermediatePosition.right = windowWidth - rect.right;
|
||||
}
|
||||
|
||||
// Устанавливаем промежуточную позицию без transition
|
||||
setPosition(intermediatePosition);
|
||||
|
||||
// Через минимальную задержку устанавливаем финальную позицию с transition
|
||||
setTimeout(() => {
|
||||
setPosition(snappedPosition);
|
||||
onPositionChange?.(snappedPosition);
|
||||
}, 0);
|
||||
} else {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const currentPosition = {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
};
|
||||
onPositionChange?.(currentPosition);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
endDrag();
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
endDrag();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
document.addEventListener("touchend", handleTouchEnd);
|
||||
document.addEventListener("touchcancel", handleTouchEnd);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.removeEventListener("touchmove", handleTouchMove);
|
||||
document.removeEventListener("touchend", handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", handleTouchEnd);
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDragging]);
|
||||
|
||||
const getContainerStyle = (): React.CSSProperties => {
|
||||
const style: React.CSSProperties = {
|
||||
position: "fixed",
|
||||
zIndex: 1000,
|
||||
};
|
||||
|
||||
if (position.top !== undefined) style.top = `${position.top}px`;
|
||||
if (position.left !== undefined) style.left = `${position.left}px`;
|
||||
if (position.right !== undefined) style.right = `${position.right}px`;
|
||||
if (position.bottom !== undefined) style.bottom = `${position.bottom}px`;
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
const getAlignmentClasses = () => {
|
||||
if (!autoAlign) return "";
|
||||
|
||||
// Во время драга используем сохраненное выравнивание
|
||||
if (isDragging) {
|
||||
return dragStartAlignment;
|
||||
}
|
||||
|
||||
// В обычном состоянии вычисляем выравнивание на основе текущей позиции
|
||||
return getAlignmentClassesFromPosition(position);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
"pointer-events-auto select-none touch-none",
|
||||
!isDragging && "transition-all duration-500 ease-out",
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab",
|
||||
getAlignmentClasses(),
|
||||
className
|
||||
)}
|
||||
style={getContainerStyle()}
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import UserCamera from "./ui/UserCamera";
|
||||
import UserDevicesControls from "./ui/UserDevicesControls";
|
||||
import clsx from "clsx";
|
||||
|
||||
const DRAG_THRESHOLD = 15;
|
||||
const OFFSET = 0.01111; // 1.111vw
|
||||
const TRANSITION = "all 0.5s cubic-bezier(.63,.08,.37,.89)";
|
||||
|
||||
export default function SessionUsersPanel() {
|
||||
const users = [
|
||||
{
|
||||
@@ -33,148 +36,150 @@ export default function SessionUsersPanel() {
|
||||
},
|
||||
];
|
||||
|
||||
function handleMute(id: number) {
|
||||
console.log(`Mute user ${id}`);
|
||||
}
|
||||
function handleVideoOff(id: number) {
|
||||
console.log(`Video off user ${id}`);
|
||||
}
|
||||
function handleCanControl(id: number) {
|
||||
console.log(`Can control user ${id}`);
|
||||
}
|
||||
|
||||
const [isTop, setIsTop] = useState(false);
|
||||
const [isLeft, setIsLeft] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
|
||||
const [corner, setCorner] = useState({ top: false, left: false });
|
||||
const [dragState, setDragState] = useState<"idle" | "dragging" | "snapping" | "released">("idle");
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
const dragStartPos = useRef({ x: 0, y: 0 });
|
||||
const isDragStarted = useRef(false);
|
||||
const DRAG_THRESHOLD = 15;
|
||||
const dragDataRef = useRef({ offsetX: 0, offsetY: 0, startX: 0, startY: 0, hasStarted: false });
|
||||
|
||||
const handleMove = (e: MouseEvent | TouchEvent) => {
|
||||
const getPointerPos = (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => ({
|
||||
x: "touches" in e ? e.touches[0].clientX : e.clientX,
|
||||
y: "touches" in e ? e.touches[0].clientY : e.clientY,
|
||||
});
|
||||
|
||||
const handleMove = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!isDragStarted.current) {
|
||||
const distance = Math.hypot(
|
||||
("clientX" in e ? e.clientX : e.touches[0].clientX) -
|
||||
dragStartPos.current.x,
|
||||
("clientY" in e ? e.clientY : e.touches[0].clientY) -
|
||||
dragStartPos.current.y
|
||||
);
|
||||
const pos = getPointerPos(e);
|
||||
const { startX, startY, offsetX, offsetY, hasStarted } = dragDataRef.current;
|
||||
|
||||
if (distance >= DRAG_THRESHOLD) {
|
||||
isDragStarted.current = true;
|
||||
setIsDragging(true);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (!hasStarted) {
|
||||
const distance = Math.hypot(pos.x - startX, pos.y - startY);
|
||||
if (distance < DRAG_THRESHOLD) return;
|
||||
|
||||
dragDataRef.current.hasStarted = true;
|
||||
setDragState("dragging");
|
||||
}
|
||||
|
||||
if (isDragStarted.current) {
|
||||
setDragPosition({
|
||||
x:
|
||||
("clientX" in e ? e.clientX : e.touches[0].clientX) -
|
||||
dragOffset.current.x,
|
||||
y:
|
||||
("clientY" in e ? e.clientY : e.touches[0].clientY) -
|
||||
dragOffset.current.y,
|
||||
setPosition({ x: pos.x - offsetX, y: pos.y - offsetY });
|
||||
}, []);
|
||||
|
||||
const handleEnd = useCallback(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
const shouldBeTop = centerY < window.innerHeight / 2;
|
||||
const shouldBeLeft = centerX < window.innerWidth / 2;
|
||||
|
||||
if (dragDataRef.current.hasStarted) {
|
||||
// Фиксируем текущую позицию без transition
|
||||
setPosition({ x: rect.left, y: rect.top });
|
||||
setDragState("released");
|
||||
|
||||
// Запускаем анимацию к углу
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setDragState("snapping");
|
||||
setCorner({ top: shouldBeTop, left: shouldBeLeft });
|
||||
setTimeout(() => setDragState("idle"), 500);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setDragState("idle");
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const center = {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
};
|
||||
const shouldBeTop = center.y < window.innerHeight / 2;
|
||||
const shouldBeLeft = center.x < window.innerWidth / 2;
|
||||
|
||||
setIsDragging(!isDragStarted.current);
|
||||
setIsTop(shouldBeTop);
|
||||
setIsLeft(shouldBeLeft);
|
||||
isDragStarted.current = false;
|
||||
|
||||
dragDataRef.current.hasStarted = false;
|
||||
window.removeEventListener("mousemove", handleMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
window.removeEventListener("touchmove", handleMove);
|
||||
window.removeEventListener("mouseup", handleEnd);
|
||||
window.removeEventListener("touchend", handleEnd);
|
||||
}, [handleMove]);
|
||||
|
||||
const handleMouseDown = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const r_pos = { x: rect.left, y: rect.top };
|
||||
const c_pos = {
|
||||
x: "clientX" in e ? e.clientX : e.touches[0].clientX,
|
||||
y: "clientY" in e ? e.clientY : e.touches[0].clientY,
|
||||
const pos = getPointerPos(e);
|
||||
|
||||
dragDataRef.current = {
|
||||
startX: pos.x,
|
||||
startY: pos.y,
|
||||
offsetX: pos.x - rect.left,
|
||||
offsetY: pos.y - rect.top,
|
||||
hasStarted: false,
|
||||
};
|
||||
|
||||
dragStartPos.current = c_pos;
|
||||
dragOffset.current = { x: c_pos.x - r_pos.x, y: c_pos.y - r_pos.y };
|
||||
setDragPosition({ x: r_pos.x, y: r_pos.y });
|
||||
setPosition({ x: rect.left, y: rect.top });
|
||||
|
||||
isDragStarted.current = false;
|
||||
window.addEventListener("mousemove", handleMove);
|
||||
window.addEventListener("touchmove", handleMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
window.addEventListener("touchend", handleMouseUp);
|
||||
window.addEventListener("mouseup", handleEnd);
|
||||
window.addEventListener("touchend", handleEnd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
window.removeEventListener("touchmove", handleMove);
|
||||
window.removeEventListener("touchend", handleMouseUp);
|
||||
window.removeEventListener("mouseup", handleEnd);
|
||||
window.removeEventListener("touchend", handleEnd);
|
||||
};
|
||||
}, []);
|
||||
}, [handleMove, handleEnd]);
|
||||
|
||||
const getStyle = (): React.CSSProperties => {
|
||||
if (isDragStarted.current && isDragging) {
|
||||
return {
|
||||
left: `${dragPosition.x}px`,
|
||||
top: `${dragPosition.y}px`,
|
||||
transition: "none",
|
||||
};
|
||||
}
|
||||
const offset = window.innerWidth * OFFSET;
|
||||
|
||||
// Вычисляем финальные координаты угла
|
||||
const getCornerPosition = () => {
|
||||
if (!containerRef.current) return { x: offset, y: offset };
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
return {
|
||||
left: isLeft ? "1.111vw" : "calc(100vw - 1.111vw)",
|
||||
top: isTop ? "1.111vw" : "calc(100vh - 1.111vw)",
|
||||
transform: `translate(${isLeft ? "0" : "-100%"}, ${
|
||||
isTop ? "0" : "-100%"
|
||||
})`,
|
||||
transition: "all 0.5s cubic-bezier(.63,.08,.37,.89)",
|
||||
x: corner.left ? offset : window.innerWidth - offset - rect.width,
|
||||
y: corner.top ? offset : window.innerHeight - offset - rect.height,
|
||||
};
|
||||
};
|
||||
|
||||
let style: React.CSSProperties;
|
||||
if (dragState === "dragging" || dragState === "released") {
|
||||
// Во время перетаскивания или сразу после отпускания
|
||||
style = {
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
transition: "none"
|
||||
};
|
||||
} else {
|
||||
// Анимация к углу или покой в углу
|
||||
const cornerPos = getCornerPosition();
|
||||
style = {
|
||||
left: cornerPos.x,
|
||||
top: cornerPos.y,
|
||||
transition: dragState === "snapping" ? TRANSITION : "none",
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleMouseDown}
|
||||
className="flex gap-4 active:cursor-grabbing cursor-grab absolute"
|
||||
style={getStyle()}
|
||||
onMouseDown={handleStart}
|
||||
onTouchStart={handleStart}
|
||||
className="flex absolute gap-4 active:cursor-grabbing cursor-grab"
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex gap-4 w-max",
|
||||
isLeft ? "flex-row-reverse" : "flex-row",
|
||||
isTop ? "items-start" : "items-end"
|
||||
corner.left ? "flex-row-reverse" : "flex-row",
|
||||
corner.top ? "items-start" : "items-end"
|
||||
)}
|
||||
>
|
||||
{users.map((user) => (
|
||||
<UserCamera
|
||||
key={user.id}
|
||||
onMute={() => handleMute(user.id)}
|
||||
onVideoOff={() => handleVideoOff(user.id)}
|
||||
onCanControl={() => handleCanControl(user.id)}
|
||||
onMute={() => console.log(`Mute user ${user.id}`)}
|
||||
onVideoOff={() => console.log(`Video off user ${user.id}`)}
|
||||
onCanControl={() => console.log(`Can control user ${user.id}`)}
|
||||
{...user}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import UserCamera from "./ui/UserCamera";
|
||||
import UserDevicesControls from "./ui/UserDevicesControls";
|
||||
import DraggableContainer from "./DraggableContainer";
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
name: "John Doe",
|
||||
isSpeaking: true,
|
||||
isMuted: false,
|
||||
isVideoOff: false,
|
||||
isControlDisabled: false,
|
||||
isAdmin: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Jane Doe",
|
||||
isSpeaking: false,
|
||||
isMuted: true,
|
||||
isVideoOff: true,
|
||||
isControlDisabled: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Jim Doe",
|
||||
isSpeaking: false,
|
||||
isMuted: false,
|
||||
isVideoOff: false,
|
||||
isControlDisabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
function SessionUsersPanel2() {
|
||||
return (
|
||||
<DraggableContainer
|
||||
enableSnapping={true}
|
||||
autoAlign={true}
|
||||
initialPosition={{ top: 20, left: 20 }}
|
||||
padding={20}
|
||||
className="flex gap-4"
|
||||
>
|
||||
{users.map((user) => (
|
||||
<UserCamera
|
||||
key={user.id}
|
||||
onMute={() => console.log(`Mute user ${user.id}`)}
|
||||
onVideoOff={() => console.log(`Video off user ${user.id}`)}
|
||||
onCanControl={() => console.log(`Can control user ${user.id}`)}
|
||||
{...user}
|
||||
/>
|
||||
))}
|
||||
|
||||
<UserDevicesControls />
|
||||
</DraggableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionUsersPanel2;
|
||||
@@ -48,8 +48,8 @@ export default function UserCamera({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"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",
|
||||
"aspect-square h-fit 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-last",
|
||||
isSpeaking
|
||||
? "ring-[0.139vw] ring-[#7B60F3]"
|
||||
: "ring-[0.069vw] ring-[#FFFFFF4D]",
|
||||
@@ -67,7 +67,7 @@ export default function UserCamera({
|
||||
|
||||
<video
|
||||
ref={ref}
|
||||
className="size-full object-cover"
|
||||
className="object-cover size-full"
|
||||
autoPlay
|
||||
muted={isMuted}
|
||||
playsInline
|
||||
|
||||
@@ -23,7 +23,7 @@ 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-last 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"
|
||||
|
||||
@@ -22,7 +22,7 @@ import { PixelStreamingWrapper } from "../components/PixelStreamingWrapper";
|
||||
import WarningIcon from "../components/icons/WarningIcon";
|
||||
import Button from "../components/ui/Button";
|
||||
import LoaderIcon from "../components/icons/LoaderIcon";
|
||||
import SessionUsersPanel from "../components/SessionUsersPanel";
|
||||
import SessionUsersPanel from "../components/SessionUsersPanel2";
|
||||
|
||||
function NewSessionPage() {
|
||||
const { setPopup, setPosition } = usePopupStore();
|
||||
@@ -126,12 +126,12 @@ function NewSessionPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-screen h-screen bg-black order-3 overflow-hidden flex justify-center_items-center">
|
||||
<div className="flex overflow-hidden relative order-3 w-screen h-screen bg-black justify-center_items-center">
|
||||
{session.status === "started" &&
|
||||
session.mode === "stream" &&
|
||||
session.server?.localIp &&
|
||||
session.playerPort && (
|
||||
<div className="aspect-video w-full h-full">
|
||||
<div className="w-full h-full aspect-video">
|
||||
<PixelStreamingWrapper
|
||||
initialSettings={{
|
||||
ss: `ws://${session.server.localIp}:${session.playerPort}`,
|
||||
@@ -189,12 +189,12 @@ function NewSessionPage() {
|
||||
</div>
|
||||
</FloatingActionButton>
|
||||
<FloatingActionButton className="2xl:hidden">
|
||||
<div className="size-4 text-white">
|
||||
<div className="text-white size-4">
|
||||
<MicrophoneFilledIcon />
|
||||
</div>
|
||||
</FloatingActionButton>
|
||||
<FloatingActionButton className="2xl:hidden">
|
||||
<div className="size-4 text-white">
|
||||
<div className="text-white size-4">
|
||||
<VideoOffFilledIcon />
|
||||
</div>
|
||||
</FloatingActionButton>
|
||||
|
||||
Reference in New Issue
Block a user