Merge pull request #478 from getmaxun/remote-resize

Feat: Resized Browser
This commit is contained in:
Karishma Shukla
2025-03-18 19:58:58 +05:30
committed by GitHub
14 changed files with 395 additions and 196 deletions

View File

@@ -36,8 +36,8 @@ const SCREENCAST_CONFIG: {
maxQueueSize: number;
} = {
format: 'jpeg',
maxWidth: 900,
maxHeight: 400,
maxWidth: 1280,
maxHeight: 720,
targetFPS: 30,
compressionQuality: 0.8,
maxQueueSize: 2
@@ -269,7 +269,7 @@ export class RemoteBrowser {
};
}
const contextOptions: any = {
viewport: { height: 400, width: 900 },
// viewport: { height: 400, width: 900 },
// recordVideo: { dir: 'videos/' }
// Force reduced motion to prevent animation issues
reducedMotion: 'reduce',
@@ -322,6 +322,15 @@ export class RemoteBrowser {
await this.setupPageEventListeners(this.currentPage);
const viewportSize = await this.currentPage.viewportSize();
if (viewportSize) {
this.socket.emit('viewportInfo', {
width: viewportSize.width,
height: viewportSize.height,
userId: this.userId
});
}
try {
const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']);
await blocker.enableBlockingInPage(this.currentPage);
@@ -335,6 +344,19 @@ export class RemoteBrowser {
}
};
public updateViewportInfo = async (): Promise<void> => {
if (this.currentPage) {
const viewportSize = await this.currentPage.viewportSize();
if (viewportSize) {
this.socket.emit('viewportInfo', {
width: viewportSize.width,
height: viewportSize.height,
userId: this.userId
});
}
}
};
/**
* Registers all event listeners needed for the recording editor session.
* Should be called only once after the full initialization of the remote browser.
@@ -452,6 +474,8 @@ export class RemoteBrowser {
// Set flag to indicate screencast is active
this.isScreencastActive = true;
await this.updateViewportInfo();
this.client.on('Page.screencastFrame', ({ data: base64, sessionId }) => {
// Only process if screencast is still active for this user
if (!this.isScreencastActive) {
@@ -563,7 +587,7 @@ export class RemoteBrowser {
const workflow = this.generator.AddGeneratedFlags(this.generator.getWorkflowFile());
await this.initializeNewPage();
if (this.currentPage) {
this.currentPage.setViewportSize({ height: 400, width: 900 });
// this.currentPage.setViewportSize({ height: 400, width: 900 });
const params = this.generator.getParams();
if (params) {
this.interpreterSettings.params = params.reduce((acc, param) => {
@@ -721,7 +745,7 @@ export class RemoteBrowser {
* @param payload the screenshot binary data
* @returns void
*/
private emitScreenshot = async (payload: Buffer): Promise<void> => {
private emitScreenshot = async (payload: Buffer, viewportSize?: { width: number, height: number }): Promise<void> => {
if (this.isProcessingScreenshot) {
if (this.screenshotQueue.length < SCREENCAST_CONFIG.maxQueueSize) {
this.screenshotQueue.push(payload);
@@ -736,11 +760,14 @@ export class RemoteBrowser {
const base64Data = optimizedScreenshot.toString('base64');
const dataWithMimeType = `data:image/jpeg;base64,${base64Data}`;
// Emit with user context to ensure the frontend can identify which browser's screenshot this is
this.socket.emit('screencast', {
image: dataWithMimeType,
userId: this.userId
}); logger.debug('Screenshot emitted');
// Emit with user context to ensure the frontend can identify which browser's screenshot this is
this.socket.emit('screencast', {
image: dataWithMimeType,
userId: this.userId,
viewport: viewportSize || await this.currentPage?.viewportSize() || null
});
logger.debug('Screenshot emitted');
} catch (error) {
logger.error('Screenshot emission failed:', error);
} finally {

View File

@@ -47,7 +47,7 @@ interface ExecuteRunData {
browserId: string;
}
const pgBoss = new PgBoss({connectionString: pgBossConnectionString, schema: 'public'});
const pgBoss = new PgBoss({connectionString: pgBossConnectionString });
/**
* Extract data safely from a job (single job or job array)

View File

@@ -11,12 +11,12 @@ import {
// TODO: Tab !show currentUrl after recordingUrl global state
export const BrowserContent = () => {
const { width } = useBrowserDimensionsStore();
const { socket } = useSocketStore();
const [tabs, setTabs] = useState<string[]>(["current"]);
const [tabIndex, setTabIndex] = React.useState(0);
const [showOutputData, setShowOutputData] = useState(false);
const { browserWidth } = useBrowserDimensionsStore();
const handleChangeIndex = useCallback(
(index: number) => {
@@ -146,7 +146,7 @@ export const BrowserContent = () => {
/>
<BrowserNavBar
// todo: use width from browser dimension once fixed
browserWidth={900}
browserWidth={browserWidth}
handleUrlChanged={handleUrlChanged}
/>

View File

@@ -32,7 +32,8 @@ export const BrowserTabs = (
return (
<Box sx={{
width: 800, // Fixed width
width: '100%',
maxWidth: '100%',
display: 'flex',
overflow: 'auto',
alignItems: 'center',

View File

@@ -9,6 +9,8 @@ import { useBrowserSteps, TextStep } from '../../context/browserSteps';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { useTranslation } from 'react-i18next';
import { AuthContext } from '../../context/auth';
import { coordinateMapper } from '../../helpers/coordinateMapper';
import { useBrowserDimensionsStore } from '../../context/browserDimensions';
interface ElementInfo {
tagName: string;
@@ -31,6 +33,12 @@ interface AttributeOption {
interface ScreencastData {
image: string;
userId: string;
viewport?: ViewportInfo | null;
}
interface ViewportInfo {
width: number;
height: number;
}
@@ -62,6 +70,7 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null):
export const BrowserWindow = () => {
const { t } = useTranslation();
const { browserWidth, browserHeight } = useBrowserDimensionsStore();
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
const [screenShot, setScreenShot] = useState<string>("");
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null);
@@ -69,6 +78,7 @@ export const BrowserWindow = () => {
const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]);
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
const [currentListId, setCurrentListId] = useState<number | null>(null);
const [viewportInfo, setViewportInfo] = useState<ViewportInfo>({ width: browserWidth, height: browserHeight });
const [listSelector, setListSelector] = useState<string | null>(null);
const [fields, setFields] = useState<Record<string, TextStep>>({});
@@ -82,6 +92,15 @@ export const BrowserWindow = () => {
const { state } = useContext(AuthContext);
const { user } = state;
const dimensions = {
width: browserWidth,
height: browserHeight
};
useEffect(() => {
coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height);
}, [viewportInfo, dimensions.width, dimensions.height]);
useEffect(() => {
if (listSelector) {
window.sessionStorage.setItem('recordingListSelector', listSelector);
@@ -130,6 +149,10 @@ export const BrowserWindow = () => {
} else if (data && typeof data === 'object' && 'image' in data) {
if (!data.userId || data.userId === user?.id) {
setScreenShot(data.image);
if (data.viewport) {
setViewportInfo(data.viewport);
}
}
}
}, [screenShot, user?.id]);
@@ -149,78 +172,85 @@ export const BrowserWindow = () => {
}, [screenShot, canvasRef, socket, screencastHandler]);
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
// Map the incoming DOMRect from browser coordinates to canvas coordinates
const mappedRect = new DOMRect(
data.rect.x,
data.rect.y,
data.rect.width,
data.rect.height
);
const mappedData = {
...data,
rect: mappedRect
};
if (getList === true) {
if (listSelector) {
socket?.emit('listSelector', { selector: listSelector });
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
const hasValidChildSelectors = Array.isArray(mappedData.childSelectors) && mappedData.childSelectors.length > 0;
if (limitMode) {
setHighlighterData(null);
} else if (paginationMode) {
// Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) {
setHighlighterData(data);
setHighlighterData(mappedData);
} else {
setHighlighterData(null);
}
} else if (data.childSelectors && data.childSelectors.includes(data.selector)) {
} else if (mappedData.childSelectors && mappedData.childSelectors.includes(mappedData.selector)) {
// Highlight only valid child elements within the listSelector
setHighlighterData(data);
} else if (data.elementInfo?.isIframeContent && data.childSelectors) {
// Handle pure iframe elements - similar to previous shadow DOM logic but using iframe syntax
// Check if the selector matches any iframe child selectors
const isIframeChild = data.childSelectors.some(childSelector =>
data.selector.includes(':>>') && // Iframe uses :>> for traversal
setHighlighterData(mappedData);
} else if (mappedData.elementInfo?.isIframeContent && mappedData.childSelectors) {
// Handle iframe elements
const isIframeChild = mappedData.childSelectors.some(childSelector =>
mappedData.selector.includes(':>>') &&
childSelector.split(':>>').some(part =>
data.selector.includes(part.trim())
mappedData.selector.includes(part.trim())
)
);
setHighlighterData(isIframeChild ? data : null);
} else if (data.selector.includes(':>>') && hasValidChildSelectors) {
setHighlighterData(isIframeChild ? mappedData : null);
} else if (mappedData.selector.includes(':>>') && hasValidChildSelectors) {
// Handle mixed DOM cases with iframes
// Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split(':>>').map(part => part.trim());
const selectorParts = mappedData.selector.split(':>>').map(part => part.trim());
const isValidMixedSelector = selectorParts.some(part =>
// We know data.childSelectors is defined due to hasValidChildSelectors check
data.childSelectors!.some(childSelector =>
mappedData.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? data : null);
} else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
// New case: Handle pure Shadow DOM elements
// Check if the selector matches any shadow root child selectors
const isShadowChild = data.childSelectors.some(childSelector =>
data.selector.includes('>>') && // Shadow DOM uses >> for piercing
setHighlighterData(isValidMixedSelector ? mappedData : null);
} else if (mappedData.elementInfo?.isShadowRoot && mappedData.childSelectors) {
// Handle Shadow DOM elements
const isShadowChild = mappedData.childSelectors.some(childSelector =>
mappedData.selector.includes('>>') &&
childSelector.split('>>').some(part =>
data.selector.includes(part.trim())
mappedData.selector.includes(part.trim())
)
);
setHighlighterData(isShadowChild ? data : null);
} else if (data.selector.includes('>>') && hasValidChildSelectors) {
// New case: Handle mixed DOM cases
// Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split('>>').map(part => part.trim());
setHighlighterData(isShadowChild ? mappedData : null);
} else if (mappedData.selector.includes('>>') && hasValidChildSelectors) {
// Handle mixed DOM cases
const selectorParts = mappedData.selector.split('>>').map(part => part.trim());
const isValidMixedSelector = selectorParts.some(part =>
// Now we know data.childSelectors is defined
data.childSelectors!.some(childSelector =>
mappedData.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? data : null);
setHighlighterData(isValidMixedSelector ? mappedData : null);
} else {
// if !valid child in normal mode, clear the highlighter
// If not a valid child in normal mode, clear the highlighter
setHighlighterData(null);
}
} else {
// Set highlighterData for the initial listSelector selection
setHighlighterData(data);
setHighlighterData(mappedData);
}
} else {
// For non-list steps
setHighlighterData(data);
setHighlighterData(mappedData);
}
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
}, [getList, socket, listSelector, paginationMode, paginationType, limitMode]);
useEffect(() => {
@@ -260,11 +290,13 @@ export const BrowserWindow = () => {
const clickY = e.clientY - canvasRect.top;
const highlightRect = highlighterData.rect;
const mappedRect = coordinateMapper.mapBrowserRectToCanvas(highlightRect);
if (
clickX >= highlightRect.left &&
clickX <= highlightRect.right &&
clickY >= highlightRect.top &&
clickY <= highlightRect.bottom
clickX >= mappedRect.left &&
clickX <= mappedRect.right &&
clickY >= mappedRect.top &&
clickY <= mappedRect.bottom
) {
const options = getAttributeOptions(highlighterData.elementInfo?.tagName || '', highlighterData.elementInfo);
@@ -437,7 +469,7 @@ export const BrowserWindow = () => {
}, [paginationMode, resetPaginationSelector]);
return (
<div onClick={handleClick} style={{ width: '900px' }} id="browser-window">
<div onClick={handleClick} style={{ width: browserWidth }} id="browser-window">
{
getText === true || getList === true ? (
<GenericModal
@@ -483,20 +515,20 @@ export const BrowserWindow = () => {
</GenericModal>
) : null
}
<div style={{ height: '400px', overflow: 'hidden' }}>
<div style={{ height: dimensions.height, overflow: 'hidden' }}>
{((getText === true || getList === true) && !showAttributeModal && highlighterData?.rect != null && highlighterData?.rect.top != null) && canvasRef?.current ?
<Highlighter
unmodifiedRect={highlighterData?.rect}
displayedSelector={highlighterData?.selector}
width={900}
height={400}
width={dimensions.width}
height={dimensions.height}
canvasRect={canvasRef.current.getBoundingClientRect()}
/>
: null}
<Canvas
onCreateRef={setCanvasReference}
width={900}
height={400}
width={dimensions.width}
height={dimensions.height}
/>
</div>
</div>
@@ -512,7 +544,7 @@ const drawImage = (image: string, canvas: HTMLCanvasElement): void => {
img.src = image;
img.onload = () => {
URL.revokeObjectURL(img.src);
ctx?.drawImage(img, 0, 0, 900, 400);
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
};
};

View File

@@ -1,5 +1,6 @@
import React from 'react';
import styled from "styled-components";
import { coordinateMapper } from '../../helpers/coordinateMapper';
interface HighlighterProps {
unmodifiedRect: DOMRect;
@@ -13,13 +14,15 @@ export const Highlighter = ({ unmodifiedRect, displayedSelector = '', width, hei
if (!unmodifiedRect) {
return null;
} else {
const mappedRect = coordinateMapper.mapBrowserRectToCanvas(unmodifiedRect);
const rect = {
top: unmodifiedRect.top + canvasRect.top + window.scrollY,
left: unmodifiedRect.left + canvasRect.left + window.scrollX,
right: unmodifiedRect.right + canvasRect.left,
bottom: unmodifiedRect.bottom + canvasRect.top,
width: unmodifiedRect.width,
height: unmodifiedRect.height,
top: mappedRect.top + canvasRect.top + window.scrollY,
left: mappedRect.left + canvasRect.left + window.scrollX,
right: mappedRect.right + canvasRect.left,
bottom: mappedRect.bottom + canvasRect.top,
width: mappedRect.width,
height: mappedRect.height,
};

View File

@@ -20,6 +20,7 @@ import { getActiveWorkflow } from "../../api/workflow";
import ActionDescriptionBox from '../action/ActionDescriptionBox';
import { useThemeMode } from '../../context/theme-provider';
import { useTranslation } from 'react-i18next';
import { useBrowserDimensionsStore } from '../../context/browserDimensions';
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
getActiveWorkflow(id).then(
@@ -54,6 +55,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const [browserStepIdList, setBrowserStepIdList] = useState<number[]>([]);
const [isCaptureTextConfirmed, setIsCaptureTextConfirmed] = useState(false);
const [isCaptureListConfirmed, setIsCaptureListConfirmed] = useState(false);
const { panelHeight } = useBrowserDimensionsStore();
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog } = useGlobalInfoStore();
const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage, showPaginationOptions, setShowPaginationOptions, showLimitOptions, setShowLimitOptions, workflow, setWorkflow } = useActionContext();
@@ -460,8 +462,9 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const theme = useThemeMode();
const isDarkMode = theme.darkMode;
return (
<Paper sx={{ height: '520px', width: 'auto', alignItems: "center", background: 'inherit' }} id="browser-actions" elevation={0}>
<Paper sx={{ height: panelHeight, width: 'auto', alignItems: "center", background: 'inherit' }} id="browser-actions" elevation={0}>
{/* <SimpleBox height={60} width='100%' background='lightGray' radius='0%'>
<Typography sx={{ padding: '10px' }}>Last action: {` ${lastAction}`}</Typography>
</SimpleBox> */}

View File

@@ -6,6 +6,7 @@ import DatePicker from '../pickers/DatePicker';
import Dropdown from '../pickers/Dropdown';
import TimePicker from '../pickers/TimePicker';
import DateTimeLocalPicker from '../pickers/DateTimeLocalPicker';
import { coordinateMapper } from '../../helpers/coordinateMapper';
interface CreateRefCallback {
(ref: React.RefObject<HTMLCanvasElement>): void;
@@ -79,7 +80,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
useEffect(() => {
if (socket) {
socket.on('showDatePicker', (info: { coordinates: Coordinates, selector: string }) => {
setDatePickerInfo(info);
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setDatePickerInfo({
...info,
coordinates: canvasCoords
});
});
socket.on('showDropdown', (info: {
@@ -92,15 +97,27 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
selected: boolean;
}>;
}) => {
setDropdownInfo(info);
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setDropdownInfo({
...info,
coordinates: canvasCoords
});
});
socket.on('showTimePicker', (info: { coordinates: Coordinates, selector: string }) => {
setTimePickerInfo(info);
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setTimePickerInfo({
...info,
coordinates: canvasCoords
});
});
socket.on('showDateTimePicker', (info: { coordinates: Coordinates, selector: string }) => {
setDateTimeLocalInfo(info);
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setDateTimeLocalInfo({
...info,
coordinates: canvasCoords
});
});
return () => {
@@ -114,13 +131,14 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
const onMouseEvent = useCallback((event: MouseEvent) => {
if (socket && canvasRef.current) {
// Get the canvas bounding rectangle
const rect = canvasRef.current.getBoundingClientRect();
const clickCoordinates = {
x: event.clientX - rect.left, // Use relative x coordinate
y: event.clientY - rect.top, // Use relative y coordinate
};
const browserCoordinates = coordinateMapper.mapCanvasToBrowser(clickCoordinates);
switch (event.type) {
case 'mousedown':
if (getTextRef.current === true) {
@@ -128,7 +146,7 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
} else if (getListRef.current === true) {
console.log('Capturing List...');
} else {
socket.emit('input:mousedown', clickCoordinates);
socket.emit('input:mousedown', browserCoordinates);
}
notifyLastAction('click');
break;
@@ -146,7 +164,7 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
x: clickCoordinates.x,
y: clickCoordinates.y,
};
socket.emit('input:mousemove', clickCoordinates);
socket.emit('input:mousemove', browserCoordinates);
notifyLastAction('move');
}
break;
@@ -173,9 +191,11 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
if (socket) {
const browserCoordinates = coordinateMapper.mapCanvasToBrowser(lastMousePosition.current);
switch (event.type) {
case 'keydown':
socket.emit('input:keydown', { key: event.key, coordinates: lastMousePosition.current });
socket.emit('input:keydown', { key: event.key, coordinates: browserCoordinates });
notifyLastAction(`${event.key} pressed`);
break;
case 'keyup':

View File

@@ -34,7 +34,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
const logEndRef = useRef<HTMLDivElement | null>(null);
const { width } = useBrowserDimensionsStore();
const { browserWidth, outputPreviewHeight, outputPreviewWidth } = useBrowserDimensionsStore();
const { socket } = useSocketStore();
const { currentWorkflowActionsState, shouldResetInterpretationLog, notify } = useGlobalInfoStore();
@@ -130,6 +130,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
return (
<Grid container>
<Grid item xs={12} md={9} lg={9}>
<div style={{ height: '20px' }}></div>
<Button
onClick={toggleDrawer(true)}
variant="contained"
@@ -141,7 +142,7 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
background: '#ff00c3',
border: 'none',
padding: '10px 20px',
width: '900px',
width: browserWidth,
overflow: 'hidden',
textAlign: 'left',
justifyContent: 'flex-start',
@@ -163,8 +164,8 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
background: `${darkMode ? '#1e2124' : 'white'}`,
color: `${darkMode ? 'white' : 'black'}`,
padding: '10px',
height: 500,
width: width - 10,
height: outputPreviewHeight,
width: outputPreviewWidth,
display: 'flex',
borderRadius: '10px 10px 0 0',
},

View File

@@ -1,6 +1,10 @@
export const VIEWPORT_W = 900;
export const VIEWPORT_H = 400;
// Default Playwright viewport dimensions
export const BROWSER_DEFAULT_WIDTH = 1280;
export const BROWSER_DEFAULT_HEIGHT = 720;
export const ONE_PERCENT_OF_VIEWPORT_W = VIEWPORT_W / 100;
export const ONE_PERCENT_OF_VIEWPORT_H = VIEWPORT_H / 100;

View File

@@ -1,39 +1,54 @@
import React, { createContext, useCallback, useContext, useState } from "react";
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
import { AppDimensions, getResponsiveDimensions } from "../helpers/dimensionUtils";
interface BrowserDimensions {
width: number;
height: number;
interface BrowserDimensionsContext extends AppDimensions {
setWidth: (newWidth: number) => void;
};
updateDimensions: () => void;
}
class BrowserDimensionsStore implements Partial<BrowserDimensions> {
width: number = 900;
height: number = 400;
};
const initialDimensions = getResponsiveDimensions();
const browserDimensionsStore = new BrowserDimensionsStore();
const browserDimensionsContext = createContext<BrowserDimensions>(browserDimensionsStore as BrowserDimensions);
const browserDimensionsContext = createContext<BrowserDimensionsContext>({
...initialDimensions,
setWidth: () => {},
updateDimensions: () => {}
});
export const useBrowserDimensionsStore = () => useContext(browserDimensionsContext);
export const BrowserDimensionsProvider = ({ children }: { children: JSX.Element }) => {
const [width, setWidth] = useState<number>(browserDimensionsStore.width);
const [height, setHeight] = useState<number>(browserDimensionsStore.height);
const [dimensions, setDimensions] = useState<AppDimensions>(initialDimensions);
const setNewWidth = useCallback((newWidth: number) => {
setWidth(newWidth);
setHeight(Math.round(newWidth / 1.6));
}, [setWidth, setHeight]);
const updateDimensions = useCallback(() => {
setDimensions(getResponsiveDimensions());
}, []);
const setWidth = useCallback((newWidth: number) => {
setDimensions((prevDimensions: any) => ({
...prevDimensions,
browserWidth: newWidth,
canvasWidth: newWidth,
browserHeight: Math.round(newWidth / 1.6),
canvasHeight: Math.round(newWidth / 1.6)
}));
}, []);
useEffect(() => {
window.addEventListener('resize', updateDimensions);
return () => {
window.removeEventListener('resize', updateDimensions);
};
}, [updateDimensions]);
return (
<browserDimensionsContext.Provider
value={{
width,
height,
setWidth: setNewWidth,
...dimensions,
setWidth,
updateDimensions
}}
>
{children}
</browserDimensionsContext.Provider>
);
};
};

View File

@@ -0,0 +1,72 @@
// coordinateMapper.ts
import { BROWSER_DEFAULT_HEIGHT, BROWSER_DEFAULT_WIDTH } from "../constants/const";
import { getResponsiveDimensions } from "./dimensionUtils";
export class CoordinateMapper {
private canvasWidth: number;
private canvasHeight: number;
private browserWidth: number;
private browserHeight: number;
constructor() {
// Use responsive dimensions instead of hardcoded values
const dimensions = getResponsiveDimensions();
this.canvasWidth = dimensions.canvasWidth;
this.canvasHeight = dimensions.canvasHeight;
this.browserWidth = BROWSER_DEFAULT_WIDTH;
this.browserHeight = BROWSER_DEFAULT_HEIGHT;
}
mapCanvasToBrowser(coord: { x: number, y: number }): { x: number, y: number } {
return {
x: (coord.x / this.canvasWidth) * this.browserWidth,
y: (coord.y / this.canvasHeight) * this.browserHeight
};
}
mapBrowserToCanvas(coord: { x: number, y: number }): { x: number, y: number } {
return {
x: (coord.x / this.browserWidth) * this.canvasWidth,
y: (coord.y / this.browserHeight) * this.canvasHeight
};
}
mapBrowserRectToCanvas(rect: DOMRect): DOMRect {
const topLeft = this.mapBrowserToCanvas({ x: rect.left, y: rect.top });
const bottomRight = this.mapBrowserToCanvas({ x: rect.right, y: rect.bottom });
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
return new DOMRect(
topLeft.x,
topLeft.y,
width,
height
);
}
mapCanvasRectToBrowser(rect: DOMRect): DOMRect {
const topLeft = this.mapCanvasToBrowser({ x: rect.left, y: rect.top });
const bottomRight = this.mapCanvasToBrowser({ x: rect.right, y: rect.bottom });
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
return new DOMRect(
topLeft.x,
topLeft.y,
width,
height
);
}
updateDimensions(canvasWidth?: number, canvasHeight?: number, browserWidth?: number, browserHeight?: number) {
if (canvasWidth) this.canvasWidth = canvasWidth;
if (canvasHeight) this.canvasHeight = canvasHeight;
if (browserWidth) this.browserWidth = browserWidth;
if (browserHeight) this.browserHeight = browserHeight;
}
}
export const coordinateMapper = new CoordinateMapper();

View File

@@ -0,0 +1,80 @@
import { useEffect, useState } from 'react';
export const WIDTH_BREAKPOINTS = {
xs: 0,
sm: 600,
md: 960,
lg: 1280,
xl: 1920
};
export const HEIGHT_BREAKPOINTS = {
xs: 0,
sm: 700,
md: 800,
lg: 900,
xl: 1080,
xxl: 1440
};
export interface AppDimensions {
browserWidth: number;
browserHeight: number;
panelHeight: number;
outputPreviewHeight: number;
outputPreviewWidth: number;
canvasWidth: number;
canvasHeight: number;
}
export const getResponsiveDimensions = (): AppDimensions => {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const browserWidth = windowWidth * 0.7;
const outputPreviewWidth = windowWidth * 0.716;
let heightFraction = 0.64;
if (windowHeight >= HEIGHT_BREAKPOINTS.xxl) {
heightFraction = 0.82;
} else if (windowHeight >= HEIGHT_BREAKPOINTS.xl) {
heightFraction = 0.76;
} else if (windowHeight >= HEIGHT_BREAKPOINTS.lg) {
heightFraction = 0.71;
} else if (windowHeight >= HEIGHT_BREAKPOINTS.md) {
heightFraction = 0.64;
} else if (windowHeight >= HEIGHT_BREAKPOINTS.sm) {
heightFraction = 0.62;
}
const browserHeight = windowHeight * heightFraction;
return {
browserWidth,
browserHeight,
panelHeight: browserHeight + 137,
outputPreviewHeight: windowHeight * 0.7,
outputPreviewWidth,
canvasWidth: browserWidth,
canvasHeight: browserHeight
};
};
// React hook to get and update dimensions on window resize
export const useDimensions = () => {
const [dimensions, setDimensions] = useState<AppDimensions>(getResponsiveDimensions());
useEffect(() => {
const handleResize = () => {
setDimensions(getResponsiveDimensions());
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return dimensions;
};

View File

@@ -1,3 +1,4 @@
/* Base styles */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@@ -13,6 +14,13 @@ body {
overflow-y: auto;
}
html {
width: 100%;
height: 100%;
overflow-y: auto;
}
/* Form element autofill styles */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
@@ -26,18 +34,12 @@ select:-webkit-autofill:focus {
transition: background-color 5000s ease-in-out 0s !important;
}
html {
width: 100%;
height: 100%;
overflow-y: auto;
}
a {
color: #ff00c3;
}
&:hover {
color: #ff00c3;
}
a:hover {
color: #ff00c3;
}
code {
@@ -46,6 +48,7 @@ code {
color: #ff00c3;
}
/* Browser-specific elements */
#browser-actions {
right: 0;
overflow-x: hidden;
@@ -55,19 +58,21 @@ code {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
box-sizing: border-box;
width: calc(100% - 4rem);
height: calc(100vh - 4rem);
margin: 2rem 2rem 2rem 2rem;
overflow: hidden;
}
#browser-content {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
flex-direction: column;
transform: scale(1);
/* Ensure no scaling */
transform-origin: top left;
/* Keep the position fixed */
}
#browser-window {
@@ -76,100 +81,36 @@ code {
}
.right-side-panel {
margin: 0;
margin-left: 1.5rem;
transform: scale(1);
transform-origin: top left;
overflow: hidden;
position: relative;
}
@media (min-width: 1024px) and (max-width: 1211px) {
.MuiButton-root[sx*="position: 'absolute'"] {
bottom: 2rem !important;
margin-bottom: 0 !important;
}
/* Consistent layout across all screen sizes */
@media screen and (min-width: 1024px) {
#browser-recorder {
box-sizing: border-box;
height: 100vh;
margin: 0;
width: calc(100% - 4rem);
height: calc(100vh - 4rem);
margin: 2rem 2rem 2rem 2rem;
}
}
/* For laptops (between 1024px and 1440px) */
@media (min-width: 1211px) and (max-width: 1440px) {
/* Adjust for very small screens */
@media screen and (max-width: 1023px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 0.6rem);
margin: 0.3rem;
width: calc(100% - 2rem);
height: calc(100vh - 3rem);
margin: 1.5rem 1rem 1.5rem 1rem;
}
}
/* For desktops (between 1441px and 1920px) */
@media (min-width: 1441px) and (max-width: 1500px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem;
}
}
@media (min-width: 1501px) and (max-width: 1700px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 8rem;
}
}
@media (min-width: 1701px) and (max-width: 1800px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 14rem;
}
}
@media (min-width: 1801px) and (max-width: 1900px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 18.5rem;
}
}
@media (min-width: 1900px) and (max-width: 1920px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 20rem;
}
}
/* For very large desktops (greater than 1920px) */
@media (min-width: 1921px) and (max-width: 2000px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 20rem;
}
}
@media (min-width: 2001px) and (max-width: 2500px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 24rem;
}
}
@media (min-width: 2501px) and (max-width: 2999px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 40rem;
}
}
@media (min-width: 3000px) {
#browser-recorder {
box-sizing: border-box;
height: calc(100vh - 2rem);
margin: 1rem 55rem;
.right-side-panel {
margin-left: 1rem;
}
}