diff --git a/client/src/MediaControl.css b/client/src/MediaControl.css index 1e4c46d..51fc3d3 100644 --- a/client/src/MediaControl.css +++ b/client/src/MediaControl.css @@ -9,12 +9,16 @@ .MediaControlSpacer { display: flex; + position: relative; + padding: 0; + margin: 0; width: 5rem; min-width: 5rem; height: 3.75rem; min-height: 3.75rem; background-color: #444; border-radius: 0.25rem; + border: 2px dashed #666; /* Visual indicator for drop zone */ } .MediaControlSpacer.Medium { @@ -24,18 +28,16 @@ min-height: 8.625em; } - .MediaControl { display: flex; position: absolute; - flex-direction: row; - justify-content: flex-end; align-items: center; width: 5rem; height: 3.75rem; min-width: 5rem; min-height: 3.75rem; z-index: 50000; + border-radius: 0.25rem; } .MediaControl .Video { diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index b1b139a..8f2356e 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -1303,26 +1303,15 @@ const MediaControl: React.FC = ({ isSelf, peer, className }) const [muted, setMuted] = useState(peer?.muted || false); const [videoOn, setVideoOn] = useState(peer?.video_on !== false); const [isValid, setIsValid] = useState(false); - const [frame, setFrame] = useState<{ translate: [number, number] }>({ translate: [0, 0] }); + const [frame, setFrame] = useState<{ + translate: [number, number]; + width?: number; + height?: number; + }>({ translate: [0, 0] }); const targetRef = useRef(null); + const spacerRef = useRef(null); const moveableRef = useRef(null); - - // Initialize position based on peer session_id to avoid stacking - useEffect(() => { - if (!peer) return; - - // Create a simple hash of the session_id to get a position offset - let hash = 0; - for (let i = 0; i < peer.session_id.length; i++) { - hash = (hash << 5) - hash + peer.session_id.charCodeAt(i); - hash = hash & hash; // Convert to 32-bit integer - } - - const x = (Math.abs(hash) % 300) + 50; // Random x between 50-350px - const y = (Math.abs(hash) % 200) + 50; // Random y between 50-250px - - setFrame({ translate: [x, y] }); - }, [peer]); + const [isDragging, setIsDragging] = useState(false); useEffect(() => { if (!peer) return; @@ -1428,7 +1417,7 @@ const MediaControl: React.FC = ({ isSelf, peer, className }) top: getComputedStyle(targetRef.current).top, transform: getComputedStyle(targetRef.current).transform, width: getComputedStyle(targetRef.current).width, - height: getComputedStyle(targetRef.current).height + height: getComputedStyle(targetRef.current).height, }); } }, [peer?.session_id]); @@ -1449,22 +1438,62 @@ const MediaControl: React.FC = ({ isSelf, peer, className }) } }; + // Snap-back functionality + const checkSnapBack = (x: number, y: number) => { + if (!spacerRef.current) return false; + + const spacerRect = spacerRef.current.getBoundingClientRect(); + const threshold = 50; // pixels from original position to trigger snap + + // Check if close to original position + const closeToOrigin = Math.abs(x) < threshold && Math.abs(y) < threshold; + + return closeToOrigin; + }; + if (!peer) return null; const colorAudio = isValid ? "primary" : "disabled"; const colorVideo = isValid ? "primary" : "disabled"; return ( - -
-
+ {/* Drop target / spacer that stays in place */} +
+ {isDragging && ( +
+ Drop here +
+ )} +
+ + {/* Moveable element - now absolute positioned */} +
@@ -1491,7 +1520,7 @@ const MediaControl: React.FC = ({ isSelf, peer, className }) autoPlay srcObject={peer.attributes.srcObject} local={peer.local} - muted={peer.local || muted} // Pass muted state + muted={peer.local || muted} /> @@ -1502,55 +1531,97 @@ const MediaControl: React.FC = ({ isSelf, peer, className }) )} - { - console.log("Moveable drag start", { target: targetRef.current, event: e }); - }} - onDrag={(e) => { - console.log("Moveable drag", { beforeTranslate: e.beforeTranslate, transform: e.transform }); - // Apply the transform directly to the target element - if (targetRef.current) { - targetRef.current.style.transform = e.transform; - } - }} - onDragEnd={(e) => { - console.log("Moveable drag end", { target: targetRef.current }); - // Get the final transform from the element - if (targetRef.current) { - const computedStyle = getComputedStyle(targetRef.current); - const transform = computedStyle.transform; - console.log("Final computed transform:", transform); - - // Parse the transform matrix to get translate values - if (transform && transform !== "none") { - const matrix = new DOMMatrix(transform); - console.log("Parsed matrix translate:", [matrix.m41, matrix.m42]); - setFrame({ translate: [matrix.m41, matrix.m42] }); - } else { - // If no transform, reset to 0,0 - setFrame({ translate: [0, 0] }); - } - } - }} - onResizeStart={(e) => { - e.setOrigin(["%", "%"]); - }} - onResize={(e) => { - e.target.style.width = `${e.width}px`; - e.target.style.height = `${e.height}px`; - }} - />
- + + { + setIsDragging(true); + }} + onDrag={(e) => { + if (targetRef.current) { + targetRef.current.style.transform = e.transform; + } + + // Check for snap-back + const matrix = new DOMMatrix(e.transform); + const shouldSnap = checkSnapBack(matrix.m41, matrix.m42); + + if (shouldSnap && spacerRef.current) { + // Add visual feedback for snap zone + spacerRef.current.style.borderColor = "#0088ff"; + } else if (spacerRef.current) { + spacerRef.current.style.borderColor = "#666"; + } + }} + onDragEnd={(e) => { + setIsDragging(false); + + if (targetRef.current) { + const computedStyle = getComputedStyle(targetRef.current); + const transform = computedStyle.transform; + + if (transform && transform !== "none") { + const matrix = new DOMMatrix(transform); + const shouldSnap = checkSnapBack(matrix.m41, matrix.m42); + + if (shouldSnap) { + // Snap back to origin + targetRef.current.style.transform = "translate(0px, 0px)"; + setFrame({ translate: [0, 0], width: frame.width, height: frame.height }); + + // Reset size if needed + if (spacerRef.current) { + const spacerRect = spacerRef.current.getBoundingClientRect(); + targetRef.current.style.width = `${spacerRect.width}px`; + targetRef.current.style.height = `${spacerRect.height}px`; + setFrame({ translate: [0, 0] }); + } + } else { + setFrame({ + translate: [matrix.m41, matrix.m42], + width: frame.width, + height: frame.height, + }); + } + } else { + setFrame({ translate: [0, 0], width: frame.width, height: frame.height }); + } + } + + // Reset spacer border color + if (spacerRef.current) { + spacerRef.current.style.borderColor = "#666"; + } + }} + onResizeStart={(e) => { + e.setOrigin(["%", "%"]); + setIsDragging(true); + }} + onResize={(e) => { + e.target.style.width = `${e.width}px`; + e.target.style.height = `${e.height}px`; + setFrame({ + ...frame, + width: e.width, + height: e.height, + }); + }} + onResizeEnd={() => { + setIsDragging(false); + }} + /> +
); }; diff --git a/client/src/UserList.css b/client/src/UserList.css index 3724680..d54c260 100644 --- a/client/src/UserList.css +++ b/client/src/UserList.css @@ -105,6 +105,7 @@ min-width: 11em; padding: 0 1px; justify-content: flex-end; + overflow: visible; } .UserList .UserSelector .UserEntry > div:first-child { diff --git a/client/src/UserList.tsx b/client/src/UserList.tsx index 7706a4a..1f6fdb7 100644 --- a/client/src/UserList.tsx +++ b/client/src/UserList.tsx @@ -155,7 +155,7 @@ const UserList: React.FC = (props: UserListProps) => { sx={{ display: "flex", flexDirection: "column", alignItems: "center" }} className={`UserEntry ${user.local ? "UserSelf" : ""}`} > -
+
{user.name ? user.name : user.session_id}
@@ -174,7 +174,7 @@ const UserList: React.FC = (props: UserListProps) => { )}
{user.bot_instance_id && ( - + {user.bot_run_id && ( = (props: UserListProps) => { > {leavingBots.has(user.session_id) ? "..." : "Leave"} - + )}
{user.name && !user.live &&
} -
+ {user.name && user.live && peers[user.session_id] && (user.local || user.has_media !== false) ? (