diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index 53e2cf3..862e177 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -1322,6 +1322,14 @@ const MediaControl: React.FC = ({ width?: number; height?: number; }>({ translate: [0, 0] }); + // Remember last released moveable position/size so we can restore to it + const lastSavedRef = useRef<{ + translate: [number, number]; + width?: number; + height?: number; + } | null>(null); + // Whether the target is currently snapped to the spacer (true) or in a free position (false) + const [isAttached, setIsAttached] = useState(true); const containerRef = useRef(null); const targetRef = useRef(null); const spacerRef = useRef(null); @@ -1341,6 +1349,40 @@ const MediaControl: React.FC = ({ }; }, []); + // Double-click toggles between spacer-attached and last saved free position + const handleDoubleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + // Ignore double-clicks on control buttons + const targetEl = e.target as HTMLElement | null; + if (targetEl && (targetEl.closest("button") || targetEl.closest(".MuiIconButton-root"))) return; + + if (!targetRef.current || !spacerRef.current) return; + + // If currently attached to spacer -> restore to last saved moveable position + if (isAttached) { + const last = lastSavedRef.current; + if (last) { + targetRef.current.style.transform = `translate(${last.translate[0]}px, ${last.translate[1]}px)`; + if (typeof last.width === "number") targetRef.current.style.width = `${last.width}px`; + if (typeof last.height === "number") targetRef.current.style.height = `${last.height}px`; + setFrame(last); + setIsAttached(false); + } + return; + } + + // If not attached -> move back to spacer (origin) + const spacerRect = spacerRef.current.getBoundingClientRect(); + targetRef.current.style.transform = "translate(0px, 0px)"; + targetRef.current.style.width = `${spacerRect.width}px`; + targetRef.current.style.height = `${spacerRect.height}px`; + setFrame({ translate: [0, 0], width: spacerRect.width, height: spacerRect.height }); + setIsAttached(true); + }, + [isAttached] + ); + useEffect(() => { console.log( `media-agent - MediaControl mounted for peer ${peer?.peer_name}, local=${peer?.local}, hasSrcObject=${!!peer @@ -1579,10 +1621,11 @@ const MediaControl: React.FC = ({ opacity: isDragging ? 1 : 0.3, transition: "opacity 0.2s", }} + onDoubleClick={handleDoubleClick} > {isDragging && ( -
= ({ fontSize: "0.7em", color: "#888", pointerEvents: "none", + userSelect: "none", }} > Drop here -
+ )} @@ -1602,6 +1646,7 @@ const MediaControl: React.FC = ({ ref={targetRef} className={`MediaControl ${className}`} data-peer={peer.session_id} + onDoubleClick={handleDoubleClick} style={{ position: "absolute", top: "0px", @@ -1651,6 +1696,7 @@ const MediaControl: React.FC = ({ srcObject={peer.attributes.srcObject} local={peer.local} muted={peer.local || muted} + onDoubleClick={handleDoubleClick} /> @@ -1711,19 +1757,29 @@ const MediaControl: React.FC = ({ const shouldSnap = checkSnapBack(matrix.m41, matrix.m42); if (shouldSnap) { targetRef.current.style.transform = "translate(0px, 0px)"; - setFrame({ translate: [0, 0], width: frame.width, height: frame.height }); + // Snap back to spacer origin + setFrame((prev) => ({ translate: [0, 0], width: prev.width, height: prev.height })); 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] }); + setFrame({ translate: [0, 0], width: spacerRect.width, height: spacerRect.height }); } + // Remember that we're attached to spacer + setIsAttached(true); } else { setFrame({ translate: [matrix.m41, matrix.m42], width: frame.width, height: frame.height, }); + // Save last free position + lastSavedRef.current = { + translate: [matrix.m41, matrix.m42], + width: frame.width, + height: frame.height, + }; + setIsAttached(false); } } else { setFrame({ translate: [0, 0], width: frame.width, height: frame.height }); @@ -1748,6 +1804,26 @@ const MediaControl: React.FC = ({ }} onResizeEnd={() => { setIsDragging(false); + // Save last size when user finishes resizing; preserve translate + if (targetRef.current) { + const computedStyle = getComputedStyle(targetRef.current); + const transform = computedStyle.transform; + let tx = 0, + ty = 0; + if (transform && transform !== "none") { + const matrix = new DOMMatrix(transform); + tx = matrix.m41; + ty = matrix.m42; + } + lastSavedRef.current = { + translate: [tx, ty], + width: frame.width, + height: frame.height, + }; + // If we resized while attached to spacer, consider that we are free + if (tx !== 0 || ty !== 0) setIsAttached(false); + else setIsAttached(true); + } }} />