From a8903cec11ef1c2a2c3dbd31bc8954727812967c Mon Sep 17 00:00:00 2001 From: JonathanGardner Date: Thu, 5 Dec 2024 04:23:16 -0500 Subject: [PATCH] Added Fullscreen and Resizing to Preview --- app/components/workbench/Preview.tsx | 258 +++++++++++++++++++++++++-- 1 file changed, 240 insertions(+), 18 deletions(-) diff --git a/app/components/workbench/Preview.tsx b/app/components/workbench/Preview.tsx index 4c1877c5..b8c2d900 100644 --- a/app/components/workbench/Preview.tsx +++ b/app/components/workbench/Preview.tsx @@ -4,11 +4,16 @@ import { IconButton } from '~/components/ui/IconButton'; import { workbenchStore } from '~/lib/stores/workbench'; import { PortDropdown } from './PortDropdown'; +type ResizeSide = 'left' | 'right' | null; + export const Preview = memo(() => { const iframeRef = useRef(null); + const containerRef = useRef(null); const inputRef = useRef(null); + const [activePreviewIndex, setActivePreviewIndex] = useState(0); const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); const hasSelectedPreview = useRef(false); const previews = useStore(workbenchStore.previews); const activePreview = previews[activePreviewIndex]; @@ -16,26 +21,40 @@ export const Preview = memo(() => { const [url, setUrl] = useState(''); const [iframeUrl, setIframeUrl] = useState(); + // Toggle between responsive mode and device mode + const [isDeviceModeOn, setIsDeviceModeOn] = useState(false); + + // Use percentage for width + const [widthPercent, setWidthPercent] = useState(37.5); // 375px assuming 1000px window width initially + // Height is always 100% + const height = '100%'; + + const resizingState = useRef({ + isResizing: false, + side: null as ResizeSide, + startX: 0, + startWidthPercent: 37.5, + windowWidth: window.innerWidth, + }); + + // Define the scaling factor + const SCALING_FACTOR = 2; // Adjust this value to increase/decrease sensitivity + useEffect(() => { if (!activePreview) { setUrl(''); setIframeUrl(undefined); - return; } const { baseUrl } = activePreview; - setUrl(baseUrl); setIframeUrl(baseUrl); - }, [activePreview, iframeUrl]); + }, [activePreview]); const validateUrl = useCallback( (value: string) => { - if (!activePreview) { - return false; - } - + if (!activePreview) return false; const { baseUrl } = activePreview; if (value === baseUrl) { @@ -56,14 +75,13 @@ export const Preview = memo(() => { [], ); - // when previews change, display the lowest port if user hasn't selected a preview + // When previews change, display the lowest port if user hasn't selected a preview useEffect(() => { if (previews.length > 1 && !hasSelectedPreview.current) { const minPortIndex = previews.reduce(findMinPortIndex, 0); - setActivePreviewIndex(minPortIndex); } - }, [previews]); + }, [previews, findMinPortIndex]); const reloadPreview = () => { if (iframeRef.current) { @@ -71,13 +89,129 @@ export const Preview = memo(() => { } }; + const toggleFullscreen = async () => { + if (!isFullscreen && containerRef.current) { + await containerRef.current.requestFullscreen(); + } else if (document.fullscreenElement) { + await document.exitFullscreen(); + } + }; + + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + + document.addEventListener('fullscreenchange', handleFullscreenChange); + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange); + }; + }, []); + + const toggleDeviceMode = () => { + setIsDeviceModeOn((prev) => !prev); + }; + + const startResizing = (e: React.MouseEvent, side: ResizeSide) => { + if (!isDeviceModeOn) return; + + // Prevent text selection + document.body.style.userSelect = 'none'; + + resizingState.current.isResizing = true; + resizingState.current.side = side; + resizingState.current.startX = e.clientX; + resizingState.current.startWidthPercent = widthPercent; + resizingState.current.windowWidth = window.innerWidth; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + + e.preventDefault(); // Prevent any text selection on mousedown + }; + + const onMouseMove = (e: MouseEvent) => { + if (!resizingState.current.isResizing) return; + const dx = e.clientX - resizingState.current.startX; + const windowWidth = resizingState.current.windowWidth; + + // Apply scaling factor to increase sensitivity + const dxPercent = ((dx / windowWidth) * 100) * SCALING_FACTOR; + + let newWidthPercent = resizingState.current.startWidthPercent; + + if (resizingState.current.side === 'right') { + newWidthPercent = resizingState.current.startWidthPercent + dxPercent; + } else if (resizingState.current.side === 'left') { + newWidthPercent = resizingState.current.startWidthPercent - dxPercent; + } + + // Clamp the width between 10% and 90% + newWidthPercent = Math.max(10, Math.min(newWidthPercent, 90)); + + setWidthPercent(newWidthPercent); + }; + + const onMouseUp = () => { + resizingState.current.isResizing = false; + resizingState.current.side = null; + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + + // Restore text selection + document.body.style.userSelect = ''; + }; + + // Handle window resize to ensure widthPercent remains valid + useEffect(() => { + const handleWindowResize = () => { + // Optional: Adjust widthPercent if necessary + // For now, since widthPercent is relative, no action is needed + }; + + window.addEventListener('resize', handleWindowResize); + return () => { + window.removeEventListener('resize', handleWindowResize); + }; + }, []); + + // A small helper component for the handle's "grip" icon + const GripIcon = () => ( +
+
+ ••• + ••• +
+
+ ); + return ( -
+
{isPortDropdownOpen && ( -
setIsPortDropdownOpen(false)} /> +
setIsPortDropdownOpen(false)} + /> )}
+
{ }} />
+ {previews.length > 1 && ( { previews={previews} /> )} + + {/* Device mode toggle button */} + + + {/* Fullscreen toggle button */} +
-
- {activePreview ? ( -