Fixed Moveable

This commit is contained in:
James Ketr 2025-09-14 14:47:05 -07:00
parent a65f46a818
commit 18a31704e8
2 changed files with 201 additions and 167 deletions

View File

@ -7,6 +7,13 @@
opacity: 1; opacity: 1;
} }
.MediaControlContainer {
position: relative; /* CRITICAL: This creates the positioning context */
display: inline-block;
width: max-content; /* Ensure container sizes to content */
height: max-content;
}
.MediaControlSpacer { .MediaControlSpacer {
display: flex; display: flex;
position: relative; position: relative;
@ -30,25 +37,34 @@
.MediaControl { .MediaControl {
display: flex; display: flex;
position: absolute; position: absolute; /* Out of flow */
align-items: center; top: 0; /* Start at top of container */
left: 0; /* Start at left of container */
width: 5rem; width: 5rem;
height: 3.75rem; height: 3.75rem;
min-width: 5rem; min-width: 5rem;
min-height: 3.75rem; min-height: 3.75rem;
z-index: 50000; z-index: 50000;
border-radius: 0.25rem; border-radius: 0.25rem;
border: 1px solid green;
} }
.MediaControl .Video { .MediaControl .Video {
position: relative; display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative;
top: 0;
left: 0;
background-color: #444; background-color: #444;
border-radius: 0.25rem; border-radius: 0.25rem;
border: 1px solid black; border: 1px solid black;
} }
video {
border: 5px solid purple;
}
.MediaControl.Medium { .MediaControl.Medium {
width: 11.5em; width: 11.5em;
height: 8.625em; height: 8.625em;
@ -56,37 +72,25 @@
min-height: 8.625em; min-height: 8.625em;
} }
.MediaControl > div {
display: flex;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
margin-right: 0.25rem;
}
.MediaControl .Controls { .MediaControl .Controls {
display: flex; display: flex;
position: absolute; position: absolute;
left: 0.5em; gap: 0;
bottom: 0.5em; left: 0;
justify-content: flex-end; bottom: 0;
flex-direction: column;
z-index: 1; z-index: 1;
align-items: center;
justify-content: center
} }
.MediaControl.Small .Controls { .MediaControl.Small .Controls {
left: 0;
bottom: unset;
justify-content: center; justify-content: center;
} }
.MediaControl .Controls > div { .MediaControl .Controls > div {
display: flex;
border-radius: 0.25em; border-radius: 0.25em;
cursor: pointer; cursor: pointer;
padding: 0.25em;
} }
.MediaControl .Controls > div:hover { .MediaControl .Controls > div:hover {

View File

@ -1308,6 +1308,7 @@ const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className })
width?: number; width?: number;
height?: number; height?: number;
}>({ translate: [0, 0] }); }>({ translate: [0, 0] });
const containerRef = useRef<HTMLDivElement>(null);
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
const spacerRef = useRef<HTMLDivElement>(null); const spacerRef = useRef<HTMLDivElement>(null);
const moveableRef = useRef<any>(null); const moveableRef = useRef<any>(null);
@ -1319,6 +1320,15 @@ const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className })
setVideoOn(peer.video_on); setVideoOn(peer.video_on);
}, [peer]); }, [peer]);
// Initialize size to match spacer
useEffect(() => {
if (spacerRef.current && targetRef.current && !frame.width) {
const spacerRect = spacerRef.current.getBoundingClientRect();
targetRef.current.style.width = `${spacerRect.width}px`;
targetRef.current.style.height = `${spacerRect.height}px`;
}
}, [frame.width]);
useEffect(() => { useEffect(() => {
if (!peer || peer.dead || !peer.attributes?.srcObject) { if (!peer || peer.dead || !peer.attributes?.srcObject) {
console.log( console.log(
@ -1457,171 +1467,191 @@ const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className })
const colorVideo = isValid ? "primary" : "disabled"; const colorVideo = isValid ? "primary" : "disabled";
return ( return (
<div className="MediaControlContainer"> <Box
{/* Drop target / spacer that stays in place */} sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
minWidth: "200px",
minHeight: "100px",
}}
>
<div <div
ref={spacerRef} ref={containerRef}
className={`MediaControlSpacer ${className}`} className="MediaControlContainer"
style={{ style={{
opacity: isDragging ? 1 : 0.3, // Make more visible when dragging position: "relative", // Ensure this is set inline too
transition: "opacity 0.2s", width: "max-content",
height: "max-content",
}} }}
> >
{isDragging && ( {/* Drop target spacer */}
<div <div
style={{ ref={spacerRef}
position: "absolute", className={`MediaControlSpacer ${className}`}
top: "50%", style={{
left: "50%", opacity: isDragging ? 1 : 0.3,
transform: "translate(-50%, -50%)", transition: "opacity 0.2s",
fontSize: "0.7em", }}
color: "#888", >
}} {isDragging && (
> <div
Drop here style={{
</div> position: "absolute",
)} top: "50%",
</div> left: "50%",
transform: "translate(-50%, -50%)",
{/* Moveable element - now absolute positioned */} fontSize: "0.7em",
<div color: "#888",
ref={targetRef} pointerEvents: "none",
className={`MediaControl ${className}`} }}
data-peer={peer.session_id} >
style={{ Drop here
left: "0px",
top: "0px",
width: frame.width ? `${frame.width}px` : undefined,
height: frame.height ? `${frame.height}px` : undefined,
transform: `translate(${frame.translate[0]}px, ${frame.translate[1]}px)`,
}}
>
<div className="Controls">
{isSelf ? (
<div onTouchStart={toggleMute} onClick={toggleMute}>
{muted ? <MicOff color={colorAudio} /> : <Mic color={colorAudio} />}
</div>
) : (
<div onTouchStart={toggleMute} onClick={toggleMute}>
{muted ? <VolumeOff color={colorAudio} /> : <VolumeUp color={colorAudio} />}
</div> </div>
)} )}
<div onTouchStart={toggleVideo} onClick={toggleVideo}>
{videoOn ? <Videocam color={colorVideo} /> : <VideocamOff color={colorVideo} />}
</div>
</div> </div>
{isValid ? (
peer.attributes?.srcObject && ( {/* Moveable element - positioned absolute relative to container */}
<Box sx={{ position: "relative" }}> <div
<Video ref={targetRef}
key={`video-${peer.session_id}-${peer.attributes.srcObject.id}`} className={`MediaControl ${className}`}
className="Video" data-peer={peer.session_id}
data-id={peer.peer_name} style={{
autoPlay position: "absolute", // Ensure this is set
srcObject={peer.attributes.srcObject} top: "0px",
local={peer.local} left: "0px",
muted={peer.local || muted} width: frame.width ? `${frame.width}px` : undefined,
/> height: frame.height ? `${frame.height}px` : undefined,
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} /> transform: `translate(${frame.translate[0]}px, ${frame.translate[1]}px)`,
</Box> }}
) >
) : ( <Box className="Controls">
<Box sx={{ position: "relative" }}> {isSelf ? (
<div className="Video">Waiting for media</div> // onTouchStart={toggleMute}
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} /> <div onClick={toggleMute}>{muted ? <MicOff color={colorAudio} /> : <Mic color={colorAudio} />}</div>
) : (
<div onClick={toggleMute}>
{muted ? <VolumeOff color={colorAudio} /> : <VolumeUp color={colorAudio} />}
</div>
)}
<div onClick={toggleVideo}>
{videoOn ? <Videocam color={colorVideo} /> : <VideocamOff color={colorVideo} />}
</div>
</Box> </Box>
)} {isValid ? (
</div> peer.attributes?.srcObject && (
<>
<Video
key={`video-${peer.session_id}-${peer.attributes.srcObject.id}`}
className="Video"
data-id={peer.peer_name}
autoPlay
srcObject={peer.attributes.srcObject}
local={peer.local}
muted={peer.local || muted}
/>
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
</>
)
) : (
<>
<div className="Video">Waiting for media</div>
<WebRTCStatus isNegotiating={peer.isNegotiating || false} connectionState={peer.connectionState} />
</>
)}
</div>
<Moveable <Moveable
ref={moveableRef} ref={moveableRef}
flushSync={flushSync} flushSync={flushSync}
pinchable={true} container={containerRef.current} // Constrain to container if needed
draggable={true} pinchable={true}
target={targetRef.current} draggable={true}
resizable={true} target={targetRef.current}
keepRatio={true} resizable={true}
hideDefaultLines={false} keepRatio={true}
snappable={true} hideDefaultLines={false}
snapThreshold={5} snappable={true}
edge snapThreshold={5}
onDragStart={(e) => { edge
setIsDragging(true); onDragStart={(e) => {
}} setIsDragging(true);
onDrag={(e) => { }}
if (targetRef.current) { onDrag={(e) => {
targetRef.current.style.transform = e.transform; if (targetRef.current) {
} targetRef.current.style.transform = e.transform;
}
// Check for snap-back // Check for snap-back
const matrix = new DOMMatrix(e.transform); const matrix = new DOMMatrix(e.transform);
const shouldSnap = checkSnapBack(matrix.m41, matrix.m42); const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
if (shouldSnap && spacerRef.current) { if (shouldSnap && spacerRef.current) {
// Add visual feedback for snap zone // Add visual feedback for snap zone
spacerRef.current.style.borderColor = "#0088ff"; spacerRef.current.style.borderColor = "#0088ff";
} else if (spacerRef.current) { } else if (spacerRef.current) {
spacerRef.current.style.borderColor = "#666"; spacerRef.current.style.borderColor = "#666";
} }
}} }}
onDragEnd={(e) => { onDragEnd={(e) => {
setIsDragging(false); setIsDragging(false);
if (targetRef.current) { if (targetRef.current) {
const computedStyle = getComputedStyle(targetRef.current); const computedStyle = getComputedStyle(targetRef.current);
const transform = computedStyle.transform; const transform = computedStyle.transform;
if (transform && transform !== "none") { if (transform && transform !== "none") {
const matrix = new DOMMatrix(transform); const matrix = new DOMMatrix(transform);
const shouldSnap = checkSnapBack(matrix.m41, matrix.m42); const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
if (shouldSnap) { if (shouldSnap) {
// Snap back to origin // Snap back to origin
targetRef.current.style.transform = "translate(0px, 0px)"; targetRef.current.style.transform = "translate(0px, 0px)";
setFrame({ translate: [0, 0], width: frame.width, height: frame.height }); setFrame({ translate: [0, 0], width: frame.width, height: frame.height });
// Reset size if needed // Reset size if needed
if (spacerRef.current) { if (spacerRef.current) {
const spacerRect = spacerRef.current.getBoundingClientRect(); const spacerRect = spacerRef.current.getBoundingClientRect();
targetRef.current.style.width = `${spacerRect.width}px`; targetRef.current.style.width = `${spacerRect.width}px`;
targetRef.current.style.height = `${spacerRect.height}px`; targetRef.current.style.height = `${spacerRect.height}px`;
setFrame({ translate: [0, 0] }); setFrame({ translate: [0, 0] });
}
} else {
setFrame({
translate: [matrix.m41, matrix.m42],
width: frame.width,
height: frame.height,
});
} }
} else { } else {
setFrame({ setFrame({ translate: [0, 0], width: frame.width, height: frame.height });
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 // Reset spacer border color
if (spacerRef.current) { if (spacerRef.current) {
spacerRef.current.style.borderColor = "#666"; spacerRef.current.style.borderColor = "#666";
} }
}} }}
onResizeStart={(e) => { onResizeStart={(e) => {
e.setOrigin(["%", "%"]); e.setOrigin(["%", "%"]);
setIsDragging(true); setIsDragging(true);
}} }}
onResize={(e) => { onResize={(e) => {
e.target.style.width = `${e.width}px`; e.target.style.width = `${e.width}px`;
e.target.style.height = `${e.height}px`; e.target.style.height = `${e.height}px`;
setFrame({ setFrame({
...frame, ...frame,
width: e.width, width: e.width,
height: e.height, height: e.height,
}); });
}} }}
onResizeEnd={() => { onResizeEnd={() => {
setIsDragging(false); setIsDragging(false);
}} }}
/> />
</div> </div>
</Box>
); );
}; };