8
mx-interpreter/index.ts
Normal file
8
mx-interpreter/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import Interpreter from './interpret';
|
||||
|
||||
export default Interpreter;
|
||||
export { default as Preprocessor } from './preprocessor';
|
||||
export type {
|
||||
WorkflowFile, WhereWhatPair, Where, What,
|
||||
} from './types/workflow';
|
||||
export { unaryOperators, naryOperators, meta as metaOperators } from './types/logic';
|
||||
85
mx-interpreter/utils/concurrency.ts
Normal file
85
mx-interpreter/utils/concurrency.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Concurrency class for running concurrent tasks while managing a limited amount of resources.
|
||||
*/
|
||||
export default class Concurrency {
|
||||
/**
|
||||
* Maximum number of workers running in parallel. If set to `null`, there is no limit.
|
||||
*/
|
||||
maxConcurrency: number = 1;
|
||||
|
||||
/**
|
||||
* Number of currently active workers.
|
||||
*/
|
||||
activeWorkers: number = 0;
|
||||
|
||||
/**
|
||||
* Queue of jobs waiting to be completed.
|
||||
*/
|
||||
private jobQueue: Function[] = [];
|
||||
|
||||
/**
|
||||
* "Resolve" callbacks of the waitForCompletion() promises.
|
||||
*/
|
||||
private waiting: Function[] = [];
|
||||
|
||||
/**
|
||||
* Constructs a new instance of concurrency manager.
|
||||
* @param {number} maxConcurrency Maximum number of workers running in parallel.
|
||||
*/
|
||||
constructor(maxConcurrency: number) {
|
||||
this.maxConcurrency = maxConcurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a waiting job out of the queue and runs it.
|
||||
*/
|
||||
private runNextJob(): void {
|
||||
const job = this.jobQueue.pop();
|
||||
|
||||
if (job) {
|
||||
// console.debug("Running a job...");
|
||||
job().then(() => {
|
||||
// console.debug("Job finished, running the next waiting job...");
|
||||
this.runNextJob();
|
||||
});
|
||||
} else {
|
||||
// console.debug("No waiting job found!");
|
||||
this.activeWorkers -= 1;
|
||||
if (this.activeWorkers === 0) {
|
||||
// console.debug("This concurrency manager is idle!");
|
||||
this.waiting.forEach((x) => x());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a job (a time-demanding async function) to the concurrency manager. \
|
||||
* The time of the job's execution depends on the concurrency manager itself
|
||||
* (given a generous enough `maxConcurrency` value, it might be immediate,
|
||||
* but this is not guaranteed).
|
||||
* @param worker Async function to be executed (job to be processed).
|
||||
*/
|
||||
addJob(job: () => Promise<any>): void {
|
||||
// console.debug("Adding a worker!");
|
||||
this.jobQueue.push(job);
|
||||
|
||||
if (!this.maxConcurrency || this.activeWorkers < this.maxConcurrency) {
|
||||
this.runNextJob();
|
||||
this.activeWorkers += 1;
|
||||
} else {
|
||||
// console.debug("No capacity to run a worker now, waiting!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until there is no running nor waiting job. \
|
||||
* If the concurrency manager is idle at the time of calling this function,
|
||||
* it waits until at least one job is compeleted (can be "presubscribed").
|
||||
* @returns Promise, resolved after there is no running/waiting worker.
|
||||
*/
|
||||
waitForCompletion(): Promise<void> {
|
||||
return new Promise((res) => {
|
||||
this.waiting.push(res);
|
||||
});
|
||||
}
|
||||
}
|
||||
30
mx-interpreter/utils/logger.ts
Normal file
30
mx-interpreter/utils/logger.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Logger class for more detailed and comprehensible logs (with colors and timestamps)
|
||||
*/
|
||||
|
||||
export enum Level {
|
||||
DATE = 36,
|
||||
LOG = 0,
|
||||
WARN = 93,
|
||||
ERROR = 31,
|
||||
DEBUG = 95,
|
||||
RESET = 0,
|
||||
}
|
||||
|
||||
export default function logger(
|
||||
message: string | Error,
|
||||
level: (Level.LOG | Level.WARN | Level.ERROR | Level.DEBUG) = Level.LOG,
|
||||
) {
|
||||
let m = message;
|
||||
if (message.constructor.name.includes('Error') && typeof message !== 'string') {
|
||||
m = <Error><unknown>(message).message;
|
||||
}
|
||||
process.stdout.write(`\x1b[${Level.DATE}m[${(new Date()).toLocaleString()}]\x1b[0m `);
|
||||
process.stdout.write(`\x1b[${level}m`);
|
||||
if (level === Level.ERROR || level === Level.WARN) {
|
||||
process.stderr.write(<string>m);
|
||||
} else {
|
||||
process.stdout.write(<string>m);
|
||||
}
|
||||
process.stdout.write(`\x1b[${Level.RESET}m\n`);
|
||||
}
|
||||
13
mx-interpreter/utils/utils.ts
Normal file
13
mx-interpreter/utils/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* ESLint rule in case there is only one util function
|
||||
* (it still does not represent the "utils" file)
|
||||
*/
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
/**
|
||||
* Converts an array of scalars to an object with **items** of the array **for keys**.
|
||||
*/
|
||||
export function arrayToObject(array : any[]) {
|
||||
return array.reduce((p, x) => ({ ...p, [x]: [] }), {});
|
||||
}
|
||||
88
package.json
Normal file
88
package.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "maxun",
|
||||
"version": "0.1.0",
|
||||
"author": "Karishma Shukla",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.5.1",
|
||||
"@mui/lab": "^5.0.0-alpha.80",
|
||||
"@mui/material": "^5.6.2",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^16.11.27",
|
||||
"@types/react": "^18.0.5",
|
||||
"@types/react-dom": "^18.0.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@wbr-project/wbr-interpret": "^0.9.3-marketa.1",
|
||||
"axios": "^0.26.0",
|
||||
"buffer": "^6.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.2",
|
||||
"fortawesome": "^0.0.1-security",
|
||||
"joi": "^17.6.0",
|
||||
"loglevel": "^1.8.0",
|
||||
"loglevel-plugin-remote": "^0.6.8",
|
||||
"playwright": "^1.18.1",
|
||||
"prismjs": "^1.28.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-highlight": "^0.14.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-simple-code-editor": "^0.11.2",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"socket.io": "^4.4.1",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"typedoc": "^0.23.8",
|
||||
"typescript": "^4.6.3",
|
||||
"uuid": "^8.3.2",
|
||||
"uuidv4": "^6.2.12",
|
||||
"web-vitals": "^2.1.4",
|
||||
"winston": "^3.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "concurrently -k \"npm run server\" \"npm run client\"",
|
||||
"server": "./node_modules/.bin/nodemon server/src/server.ts",
|
||||
"client": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "./node_modules/.bin/eslint ."
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/loglevel": "^1.6.3",
|
||||
"@types/node": "^17.0.15",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react-highlight": "^0.12.5",
|
||||
"@types/react-transition-group": "^4.4.4",
|
||||
"@types/styled-components": "^5.1.23",
|
||||
"concurrently": "^7.0.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"ts-node": "^10.4.0"
|
||||
}
|
||||
}
|
||||
125
src/components/atoms/Highlighter.tsx
Normal file
125
src/components/atoms/Highlighter.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
|
||||
import styled from "styled-components";
|
||||
|
||||
interface HighlighterProps {
|
||||
unmodifiedRect: DOMRect;
|
||||
displayedSelector: string;
|
||||
width: number;
|
||||
height: number;
|
||||
canvasRect: DOMRect;
|
||||
};
|
||||
|
||||
export const Highlighter = ({ unmodifiedRect, displayedSelector = '', width, height, canvasRect }: HighlighterProps) => {
|
||||
if (!unmodifiedRect) {
|
||||
return null;
|
||||
} else {
|
||||
// const unshiftedRect = mapRect(unmodifiedRect, width, height);
|
||||
// console.log('unshiftedRect', unshiftedRect)
|
||||
// const rect = {
|
||||
// bottom: unshiftedRect.bottom + canvasRect.top,
|
||||
// top: unshiftedRect.top + canvasRect.top,
|
||||
// left: unshiftedRect.left + canvasRect.left,
|
||||
// right: unshiftedRect.right + canvasRect.left,
|
||||
// x: unshiftedRect.x + canvasRect.left,
|
||||
// y: unshiftedRect.y + canvasRect.top,
|
||||
// width: unshiftedRect.width,
|
||||
// height: unshiftedRect.height,
|
||||
// }
|
||||
|
||||
const rect = {
|
||||
top: unmodifiedRect.top + canvasRect.top,
|
||||
left: unmodifiedRect.left + canvasRect.left,
|
||||
right: unmodifiedRect.right + canvasRect.left,
|
||||
bottom: unmodifiedRect.bottom + canvasRect.top,
|
||||
width: unmodifiedRect.width,
|
||||
height: unmodifiedRect.height,
|
||||
};
|
||||
|
||||
const adjustedWidth = Math.min(rect.width, width - rect.left); // Adjust width if it extends beyond canvas boundary
|
||||
const adjustedHeight = Math.min(rect.height, height - rect.top); // Adjust height if it extends beyond canvas boundary
|
||||
|
||||
console.log('unmodifiedRect:', unmodifiedRect)
|
||||
console.log('rectangle:', rect)
|
||||
console.log('canvas rectangle:', canvasRect)
|
||||
|
||||
// make the highlighting rectangle stay in browser window boundaries
|
||||
// if (rect.bottom > canvasRect.bottom) {
|
||||
// rect.height = height - unshiftedRect.top;
|
||||
// }
|
||||
|
||||
// if (rect.top < canvasRect.top) {
|
||||
// rect.height = rect.height - (canvasRect.top - rect.top);
|
||||
// rect.top = canvasRect.top;
|
||||
// }
|
||||
|
||||
// if (rect.right > canvasRect.right) {
|
||||
// rect.width = width - unshiftedRect.left;
|
||||
// }
|
||||
|
||||
// if (rect.left < canvasRect.left) {
|
||||
// rect.width = rect.width - (canvasRect.left - rect.left);
|
||||
// rect.left = canvasRect.left;
|
||||
// }
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HighlighterOutline
|
||||
id="Highlighter-outline"
|
||||
top={rect.top}
|
||||
left={rect.left}
|
||||
width={rect.width}
|
||||
height={rect.height}
|
||||
/>
|
||||
<HighlighterLabel
|
||||
id="Highlighter-label"
|
||||
top={rect.top + rect.height + 8}
|
||||
left={rect.left}
|
||||
>
|
||||
{displayedSelector}
|
||||
</HighlighterLabel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HighlighterOutline = styled.div<HighlighterOutlineProps>`
|
||||
box-sizing: border-box;
|
||||
pointer-events: none !important;
|
||||
position: fixed !important;
|
||||
background: #ff5d5b26 !important;
|
||||
outline: 4px solid pink !important;
|
||||
// border: 4px solid #ff5d5b !important;
|
||||
z-index: 2147483647 !important;
|
||||
// border-radius: 5px;
|
||||
top: ${(p: HighlighterOutlineProps) => p.top}px;
|
||||
left: ${(p: HighlighterOutlineProps) => p.left}px;
|
||||
width: ${(p: HighlighterOutlineProps) => p.width}px;
|
||||
height: ${(p: HighlighterOutlineProps) => p.height}px;
|
||||
`;
|
||||
|
||||
const HighlighterLabel = styled.div<HighlighterLabelProps>`
|
||||
pointer-events: none !important;
|
||||
position: fixed !important;
|
||||
background: #080a0b !important;
|
||||
color: white !important;
|
||||
padding: 8px !important;
|
||||
font-family: monospace !important;
|
||||
border-radius: 5px !important;
|
||||
z-index: 2147483647 !important;
|
||||
top: ${(p: HighlighterLabelProps) => p.top}px;
|
||||
left: ${(p: HighlighterLabelProps) => p.left}px;
|
||||
`;
|
||||
|
||||
interface HighlighterLabelProps {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
interface HighlighterOutlineProps {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
47
src/components/atoms/buttons/buttons.tsx
Normal file
47
src/components/atoms/buttons/buttons.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const NavBarButton = styled.button<{ disabled: boolean }>`
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: ${({ disabled }) => disabled ? 'default' : 'pointer'};
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
color: ${({ disabled }) => disabled ? '#999' : '#333'};
|
||||
|
||||
${({ disabled }) => disabled ? null : `
|
||||
&:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
&:active {
|
||||
background-color: #d0d0d0;
|
||||
}
|
||||
`};
|
||||
`;
|
||||
|
||||
export const UrlFormButton = styled.button`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
color: #333;
|
||||
|
||||
&:hover {
|
||||
background-color: #ddd;
|
||||
},
|
||||
|
||||
&:active {
|
||||
background-color: #d0d0d0;
|
||||
},
|
||||
`;
|
||||
145
src/components/atoms/canvas.tsx
Normal file
145
src/components/atoms/canvas.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, {useCallback, useEffect, useRef} from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { getMappedCoordinates } from "../../helpers/inputHelpers";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
|
||||
interface CreateRefCallback {
|
||||
|
||||
(ref: React.RefObject<HTMLCanvasElement>): void;
|
||||
|
||||
}
|
||||
|
||||
interface CanvasProps {
|
||||
|
||||
width: number;
|
||||
|
||||
height: number;
|
||||
|
||||
onCreateRef: CreateRefCallback;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for mouse's x,y coordinates
|
||||
*/
|
||||
export interface Coordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const { socket } = useSocketStore();
|
||||
const { setLastAction, lastAction } = useGlobalInfoStore();
|
||||
|
||||
const notifyLastAction = (action: string) => {
|
||||
if (lastAction !== action) {
|
||||
setLastAction(action);
|
||||
}
|
||||
};
|
||||
|
||||
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
|
||||
//const lastWheelPosition = useRef<ScrollDeltas>({ deltaX: 0, deltaY: 0 });
|
||||
|
||||
const onMouseEvent = useCallback((event: MouseEvent) => {
|
||||
if (socket) {
|
||||
const coordinates = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
}
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
const clickCoordinates = getMappedCoordinates(event, canvasRef.current, width, height);
|
||||
socket.emit('input:mousedown', clickCoordinates);
|
||||
notifyLastAction('click');
|
||||
break;
|
||||
case 'mousemove':
|
||||
const coordinates = getMappedCoordinates(event, canvasRef.current, width, height);
|
||||
if (lastMousePosition.current.x !== coordinates.x ||
|
||||
lastMousePosition.current.y !== coordinates.y) {
|
||||
lastMousePosition.current = {
|
||||
x: coordinates.x,
|
||||
y: coordinates.y,
|
||||
};
|
||||
socket.emit('input:mousemove', {
|
||||
x: coordinates.x,
|
||||
y: coordinates.y,
|
||||
});
|
||||
notifyLastAction('move');
|
||||
}
|
||||
break;
|
||||
case 'wheel':
|
||||
const wheelEvent = event as WheelEvent;
|
||||
const deltas = {
|
||||
deltaX: Math.round(wheelEvent.deltaX),
|
||||
deltaY: Math.round(wheelEvent.deltaY),
|
||||
};
|
||||
socket.emit('input:wheel', deltas);
|
||||
notifyLastAction('scroll');
|
||||
break;
|
||||
default:
|
||||
console.log('Default mouseEvent registered');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||
if (socket) {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
socket.emit('input:keydown', { key: event.key, coordinates: lastMousePosition.current });
|
||||
notifyLastAction(`${event.key} pressed`);
|
||||
break;
|
||||
case 'keyup':
|
||||
socket.emit('input:keyup', event.key);
|
||||
break;
|
||||
default:
|
||||
console.log('Default keyEvent registered');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
onCreateRef(canvasRef);
|
||||
canvasRef.current.addEventListener('mousedown', onMouseEvent);
|
||||
canvasRef.current.addEventListener('mousemove', onMouseEvent);
|
||||
canvasRef.current.addEventListener('wheel', onMouseEvent, { passive: true });
|
||||
canvasRef.current.addEventListener('keydown', onKeyboardEvent);
|
||||
canvasRef.current.addEventListener('keyup', onKeyboardEvent);
|
||||
|
||||
return () => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.removeEventListener('mousedown', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('mousemove', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('wheel', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('keydown', onKeyboardEvent);
|
||||
canvasRef.current.removeEventListener('keyup', onKeyboardEvent);
|
||||
}
|
||||
|
||||
};
|
||||
}else {
|
||||
console.log('Canvas not initialized');
|
||||
}
|
||||
|
||||
}, [onMouseEvent]);
|
||||
|
||||
return (
|
||||
// <canvas tabIndex={0} ref={canvasRef} height={height} width={width} />
|
||||
<canvas
|
||||
tabIndex={0}
|
||||
ref={canvasRef}
|
||||
height={720}
|
||||
width={1280}
|
||||
style={{ width: '1280px', height: '720px' }} // Ensure dimensions are explicitly set
|
||||
/>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
export default Canvas;
|
||||
19
src/components/atoms/form.tsx
Normal file
19
src/components/atoms/form.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const NavBarForm = styled.form`
|
||||
flex: 1px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const NavBarInput = styled.input`
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
padding-left: 12px;
|
||||
padding-right: 40px;
|
||||
`;
|
||||
123
src/components/molecules/BrowserNavBar.tsx
Normal file
123
src/components/molecules/BrowserNavBar.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
FC,
|
||||
} from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
|
||||
import { NavBarButton } from '../atoms/buttons/buttons';
|
||||
import { UrlForm } from './UrlForm';
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {useSocketStore} from "../../context/socket";
|
||||
import { getCurrentUrl } from "../../api/recording";
|
||||
|
||||
const StyledNavBar = styled.div<{ browserWidth: number }>`
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
background-color: #f6f6f6;
|
||||
width: ${({ browserWidth }) => browserWidth}px;
|
||||
`;
|
||||
|
||||
interface NavBarProps {
|
||||
browserWidth: number;
|
||||
handleUrlChanged: (url: string) => void;
|
||||
};
|
||||
|
||||
const BrowserNavBar: FC<NavBarProps> = ({
|
||||
browserWidth,
|
||||
handleUrlChanged,
|
||||
}) => {
|
||||
|
||||
// context:
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const [currentUrl, setCurrentUrl] = useState<string>('https://');
|
||||
|
||||
const handleRefresh = useCallback(() : void => {
|
||||
socket?.emit('input:refresh');
|
||||
}, [socket]);
|
||||
|
||||
const handleGoTo = useCallback((address: string) : void => {
|
||||
socket?.emit('input:url', address);
|
||||
}, [socket]);
|
||||
|
||||
const handleCurrentUrlChange = useCallback((url: string) => {
|
||||
handleUrlChanged(url);
|
||||
setCurrentUrl(url);
|
||||
}, [handleUrlChanged, currentUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentUrl().then((response) => {
|
||||
if (response) {
|
||||
handleUrlChanged(response);
|
||||
setCurrentUrl(response);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.log("Fetching current url failed");
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on('urlChanged', handleCurrentUrlChange);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('urlChanged', handleCurrentUrlChange);
|
||||
}
|
||||
}
|
||||
}, [socket, handleCurrentUrlChange])
|
||||
|
||||
const addAddress = (address: string) => {
|
||||
if (socket) {
|
||||
handleUrlChanged(address);
|
||||
handleGoTo(address);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledNavBar browserWidth={browserWidth}>
|
||||
<NavBarButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
socket?.emit('input:back');
|
||||
}}
|
||||
disabled={false}
|
||||
>
|
||||
<ArrowBackIcon/>
|
||||
</NavBarButton>
|
||||
|
||||
<NavBarButton
|
||||
type="button"
|
||||
onClick={()=>{
|
||||
socket?.emit('input:forward');
|
||||
}}
|
||||
disabled={false}
|
||||
>
|
||||
<ArrowForwardIcon/>
|
||||
</NavBarButton>
|
||||
|
||||
<NavBarButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (socket) {
|
||||
handleRefresh()
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
>
|
||||
<ReplayIcon/>
|
||||
</NavBarButton>
|
||||
|
||||
<UrlForm
|
||||
currentAddress={currentUrl}
|
||||
handleRefresh={handleRefresh}
|
||||
setCurrentAddress={addAddress}
|
||||
/>
|
||||
</StyledNavBar>
|
||||
);
|
||||
}
|
||||
|
||||
export default BrowserNavBar;
|
||||
93
src/components/molecules/BrowserTabs.tsx
Normal file
93
src/components/molecules/BrowserTabs.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import { Box, IconButton, Tab, Tabs } from "@mui/material";
|
||||
import { AddButton } from "../atoms/buttons/AddButton";
|
||||
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
|
||||
import { Close } from "@mui/icons-material";
|
||||
|
||||
interface BrowserTabsProp {
|
||||
tabs: string[],
|
||||
handleTabChange: (index: number) => void,
|
||||
handleAddNewTab: () => void,
|
||||
handleCloseTab: (index: number) => void,
|
||||
handleChangeIndex: (index: number) => void;
|
||||
tabIndex: number
|
||||
}
|
||||
|
||||
export const BrowserTabs = (
|
||||
{
|
||||
tabs, handleTabChange, handleAddNewTab,
|
||||
handleCloseTab, handleChangeIndex, tabIndex
|
||||
}: BrowserTabsProp) => {
|
||||
|
||||
let tabWasClosed = false;
|
||||
|
||||
const { width } = useBrowserDimensionsStore();
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
if (!tabWasClosed) {
|
||||
handleChangeIndex(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: `${width}px`,
|
||||
display: 'flex',
|
||||
overflow: 'auto',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs
|
||||
value={tabIndex}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<Tab
|
||||
key={`tab-${index}`}
|
||||
id={`tab-${index}`}
|
||||
icon={<CloseButton closeTab={() => {
|
||||
tabWasClosed = true;
|
||||
handleCloseTab(index);
|
||||
}} disabled={tabs.length === 1}
|
||||
/>}
|
||||
iconPosition="end"
|
||||
onClick={() => {
|
||||
if (!tabWasClosed) {
|
||||
handleTabChange(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
label={tab}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</Box>
|
||||
<AddButton handleClick={handleAddNewTab} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface CloseButtonProps {
|
||||
closeTab: () => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const CloseButton = ({ closeTab, disabled }: CloseButtonProps) => {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
size={"small"}
|
||||
onClick={closeTab}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
height: '34px',
|
||||
'&:hover': { color: 'white', backgroundColor: '#1976d2' }
|
||||
}}
|
||||
component="span"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
73
src/components/molecules/UrlForm.tsx
Normal file
73
src/components/molecules/UrlForm.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useState, useCallback, useEffect, } from 'react';
|
||||
import type { SyntheticEvent, } from 'react';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
|
||||
import { NavBarForm, NavBarInput } from "../atoms/form";
|
||||
import { UrlFormButton } from "../atoms/buttons/buttons";
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Socket } from "socket.io-client";
|
||||
|
||||
type Props = {
|
||||
currentAddress: string;
|
||||
handleRefresh: (socket: Socket) => void;
|
||||
setCurrentAddress: (address: string) => void;
|
||||
};
|
||||
|
||||
export const UrlForm = ({
|
||||
currentAddress,
|
||||
handleRefresh,
|
||||
setCurrentAddress,
|
||||
}: Props) => {
|
||||
// states:
|
||||
const [address, setAddress] = useState<string>(currentAddress);
|
||||
// context:
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const areSameAddresses = address === currentAddress;
|
||||
|
||||
const onChange = useCallback((event: SyntheticEvent): void => {
|
||||
setAddress((event.target as HTMLInputElement).value);
|
||||
}, [address]);
|
||||
|
||||
const onSubmit = (event: SyntheticEvent): void => {
|
||||
event.preventDefault();
|
||||
let url = address;
|
||||
|
||||
// add protocol if missing
|
||||
if (!/^(?:f|ht)tps?\:\/\//.test(address)) {
|
||||
url = "https://" + address;
|
||||
setAddress(url);
|
||||
}
|
||||
|
||||
if (areSameAddresses) {
|
||||
if (socket) {
|
||||
handleRefresh(socket);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// try the validity of url
|
||||
new URL(url);
|
||||
setCurrentAddress(url);
|
||||
} catch (e) {
|
||||
alert(`ERROR: ${url} is not a valid url!`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(currentAddress)
|
||||
}, [currentAddress]);
|
||||
|
||||
return (
|
||||
<NavBarForm onSubmit={onSubmit}>
|
||||
<NavBarInput
|
||||
type="text"
|
||||
value={address}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<UrlFormButton type="submit">
|
||||
<KeyboardArrowRightIcon />
|
||||
</UrlFormButton>
|
||||
</NavBarForm>
|
||||
);
|
||||
};
|
||||
136
src/components/organisms/BrowserContent.tsx
Normal file
136
src/components/organisms/BrowserContent.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styled from "styled-components";
|
||||
import BrowserNavBar from "../molecules/BrowserNavBar";
|
||||
import { BrowserWindow } from "./BrowserWindow";
|
||||
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
|
||||
import { BrowserTabs } from "../molecules/BrowserTabs";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { getCurrentTabs, getCurrentUrl, interpretCurrentRecording } from "../../api/recording";
|
||||
|
||||
export const BrowserContent = () => {
|
||||
const { width } = useBrowserDimensionsStore();
|
||||
const { socket } = useSocketStore();
|
||||
|
||||
const [tabs, setTabs] = useState<string[]>(['current']);
|
||||
const [tabIndex, setTabIndex] = React.useState(0);
|
||||
|
||||
const handleChangeIndex = useCallback((index: number) => {
|
||||
setTabIndex(index);
|
||||
}, [tabIndex])
|
||||
|
||||
const handleCloseTab = useCallback((index: number) => {
|
||||
// the tab needs to be closed on the backend
|
||||
socket?.emit('closeTab', {
|
||||
index,
|
||||
isCurrent: tabIndex === index,
|
||||
});
|
||||
// change the current index as current tab gets closed
|
||||
if (tabIndex === index) {
|
||||
if (tabs.length > index + 1) {
|
||||
handleChangeIndex(index);
|
||||
} else {
|
||||
handleChangeIndex(index - 1);
|
||||
}
|
||||
} else {
|
||||
handleChangeIndex(tabIndex - 1);
|
||||
}
|
||||
// update client tabs
|
||||
setTabs((prevState) => [
|
||||
...prevState.slice(0, index),
|
||||
...prevState.slice(index + 1)
|
||||
])
|
||||
}, [tabs, socket, tabIndex]);
|
||||
|
||||
const handleAddNewTab = useCallback(() => {
|
||||
// Adds new tab by pressing the plus button
|
||||
socket?.emit('addTab');
|
||||
// Adds a new tab to the end of the tabs array and shifts focus
|
||||
setTabs((prevState) => [...prevState, 'new tab']);
|
||||
handleChangeIndex(tabs.length);
|
||||
}, [socket, tabs]);
|
||||
|
||||
const handleNewTab = useCallback((tab: string) => {
|
||||
// Adds a new tab to the end of the tabs array and shifts focus
|
||||
setTabs((prevState) => [...prevState, tab]);
|
||||
// changes focus on the new tab - same happens in the remote browser
|
||||
handleChangeIndex(tabs.length);
|
||||
handleTabChange(tabs.length);
|
||||
}, [tabs]);
|
||||
|
||||
const handleTabChange = useCallback((index: number) => {
|
||||
// page screencast and focus needs to be changed on backend
|
||||
socket?.emit('changeTab', index);
|
||||
}, [socket]);
|
||||
|
||||
const handleUrlChanged = (url: string) => {
|
||||
const parsedUrl = new URL(url);
|
||||
if (parsedUrl.hostname) {
|
||||
const host = parsedUrl.hostname.match(/\b(?!www\.)[a-zA-Z0-9]+/g)?.join('.')
|
||||
if (host && host !== tabs[tabIndex]) {
|
||||
setTabs((prevState) => [
|
||||
...prevState.slice(0, tabIndex),
|
||||
host,
|
||||
...prevState.slice(tabIndex + 1)
|
||||
])
|
||||
}
|
||||
} else {
|
||||
if (tabs[tabIndex] !== 'new tab') {
|
||||
setTabs((prevState) => [
|
||||
...prevState.slice(0, tabIndex),
|
||||
'new tab',
|
||||
...prevState.slice(tabIndex + 1)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const tabHasBeenClosedHandler = useCallback((index: number) => {
|
||||
handleCloseTab(index);
|
||||
}, [handleCloseTab])
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on('newTab', handleNewTab);
|
||||
socket.on('tabHasBeenClosed', tabHasBeenClosedHandler);
|
||||
}
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('newTab', handleNewTab);
|
||||
socket.off('tabHasBeenClosed', tabHasBeenClosedHandler);
|
||||
}
|
||||
}
|
||||
}, [socket, handleNewTab])
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentTabs().then((response) => {
|
||||
if (response) {
|
||||
setTabs(response);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.log("Fetching current url failed");
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BrowserContentWrapper>
|
||||
<BrowserTabs
|
||||
tabs={tabs}
|
||||
handleTabChange={handleTabChange}
|
||||
handleAddNewTab={handleAddNewTab}
|
||||
handleCloseTab={handleCloseTab}
|
||||
handleChangeIndex={handleChangeIndex}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
<BrowserNavBar
|
||||
browserWidth={width - 10}
|
||||
handleUrlChanged={handleUrlChanged}
|
||||
/>
|
||||
<BrowserWindow/>
|
||||
</BrowserContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const BrowserContentWrapper = styled.div`
|
||||
grid-area: browser;
|
||||
`;
|
||||
106
src/components/organisms/BrowserWindow.tsx
Normal file
106
src/components/organisms/BrowserWindow.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import Canvas from "../atoms/canvas";
|
||||
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
|
||||
import { Highlighter } from "../atoms/Highlighter";
|
||||
|
||||
export const BrowserWindow = () => {
|
||||
|
||||
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
|
||||
const [screenShot, setScreenShot] = useState<string>("");
|
||||
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string } | null>(null);
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
const { width, height } = useBrowserDimensionsStore();
|
||||
|
||||
console.log('Use browser dimensions:', width, height)
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (canvasRef && canvasRef.current && highlighterData) {
|
||||
const canvasRect = canvasRef.current.getBoundingClientRect();
|
||||
// mousemove outside the browser window
|
||||
if (
|
||||
e.pageX < canvasRect.left
|
||||
|| e.pageX > canvasRect.right
|
||||
|| e.pageY < canvasRect.top
|
||||
|| e.pageY > canvasRect.bottom
|
||||
) {
|
||||
setHighlighterData(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const screencastHandler = useCallback((data: string) => {
|
||||
setScreenShot(data);
|
||||
}, [screenShot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on("screencast", screencastHandler);
|
||||
}
|
||||
if (canvasRef?.current) {
|
||||
drawImage(screenShot, canvasRef.current);
|
||||
} else {
|
||||
console.log('Canvas is not initialized');
|
||||
}
|
||||
return () => {
|
||||
socket?.off("screencast", screencastHandler);
|
||||
}
|
||||
|
||||
}, [screenShot, canvasRef, socket, screencastHandler]);
|
||||
|
||||
|
||||
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string }) => {
|
||||
setHighlighterData(data);
|
||||
console.log('Highlighter Rect via socket:', data.rect)
|
||||
}, [highlighterData])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousemove', onMouseMove, false);
|
||||
if (socket) {
|
||||
socket.on("highlighter", highlighterHandler);
|
||||
}
|
||||
//cleaning function
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
socket?.off("highlighter", highlighterHandler);
|
||||
};
|
||||
}, [socket, onMouseMove]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(highlighterData?.rect != null && highlighterData?.rect.top != null) && canvasRef?.current ?
|
||||
< Highlighter
|
||||
unmodifiedRect={highlighterData?.rect}
|
||||
displayedSelector={highlighterData?.selector}
|
||||
width={width}
|
||||
height={height}
|
||||
canvasRect={canvasRef.current.getBoundingClientRect()}
|
||||
/>
|
||||
: null}
|
||||
<Canvas
|
||||
onCreateRef={setCanvasReference}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const drawImage = (image: string, canvas: HTMLCanvasElement): void => {
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const img = new Image();
|
||||
|
||||
img.src = image;
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(img.src);
|
||||
//ctx?.clearRect(0, 0, canvas?.width || 0, VIEWPORT_H || 0);
|
||||
// ctx?.drawImage(img, 0, 0, canvas.width , canvas.height);
|
||||
ctx?.drawImage(img, 0, 0, 1280, 720); // Explicitly draw image at 1280 x 720
|
||||
console.log('Image drawn on canvas:', img.width, img.height);
|
||||
console.log('Image drawn on canvas:', canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
};
|
||||
39
src/context/browserDimensions.tsx
Normal file
39
src/context/browserDimensions.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { createContext, useCallback, useContext, useState } from "react";
|
||||
|
||||
interface BrowserDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
setWidth: (newWidth: number) => void;
|
||||
};
|
||||
|
||||
class BrowserDimensionsStore implements Partial<BrowserDimensions>{
|
||||
width: number = 1280;
|
||||
height: number = 720;
|
||||
};
|
||||
|
||||
const browserDimensionsStore = new BrowserDimensionsStore();
|
||||
const browserDimensionsContext = createContext<BrowserDimensions>(browserDimensionsStore as BrowserDimensions);
|
||||
|
||||
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 setNewWidth = useCallback((newWidth: number) => {
|
||||
setWidth(newWidth);
|
||||
setHeight(Math.round(newWidth / 1.6));
|
||||
}, [setWidth, setHeight]);
|
||||
|
||||
return (
|
||||
<browserDimensionsContext.Provider
|
||||
value={{
|
||||
width,
|
||||
height,
|
||||
setWidth: setNewWidth,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</browserDimensionsContext.Provider>
|
||||
);
|
||||
};
|
||||
141
src/helpers/inputHelpers.ts
Normal file
141
src/helpers/inputHelpers.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
VIEWPORT_W,
|
||||
VIEWPORT_H,
|
||||
} from "../constants/const";
|
||||
import { Coordinates } from '../components/atoms/canvas';
|
||||
|
||||
export const throttle = (callback: any, limit: number) => {
|
||||
let wait = false;
|
||||
return (...args: any[]) => {
|
||||
if (!wait) {
|
||||
callback(...args);
|
||||
wait = true;
|
||||
setTimeout(function () {
|
||||
wait = false;
|
||||
}, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getMappedCoordinates = (
|
||||
event: MouseEvent,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
browserWidth: number,
|
||||
browserHeight: number,
|
||||
): Coordinates => {
|
||||
const clientCoordinates = getCoordinates(event, canvas);
|
||||
const mappedX = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
clientCoordinates.x,
|
||||
);
|
||||
const mappedY = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
clientCoordinates.y,
|
||||
);
|
||||
|
||||
return {
|
||||
x: mappedX,
|
||||
y: mappedY
|
||||
};
|
||||
};
|
||||
|
||||
const getCoordinates = (event: MouseEvent, canvas: HTMLCanvasElement | null): Coordinates => {
|
||||
if (!canvas) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
return {
|
||||
x: event.pageX - canvas.offsetLeft,
|
||||
y: event.pageY - canvas.offsetTop
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRect = (
|
||||
rect: DOMRect,
|
||||
browserWidth: number,
|
||||
browserHeight: number,
|
||||
) => {
|
||||
const mappedX = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.x,
|
||||
);
|
||||
const mappedLeft = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.left,
|
||||
);
|
||||
const mappedRight = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.right,
|
||||
);
|
||||
const mappedWidth = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.width,
|
||||
);
|
||||
const mappedY = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.y,
|
||||
);
|
||||
const mappedTop = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.top,
|
||||
);
|
||||
const mappedBottom = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.bottom,
|
||||
);
|
||||
const mappedHeight = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.height,
|
||||
);
|
||||
|
||||
console.log('Mapped:', {
|
||||
x: mappedX,
|
||||
y: mappedY,
|
||||
width: mappedWidth,
|
||||
height: mappedHeight,
|
||||
top: mappedTop,
|
||||
right: mappedRight,
|
||||
bottom: mappedBottom,
|
||||
left: mappedLeft,
|
||||
})
|
||||
|
||||
return {
|
||||
x: mappedX,
|
||||
y: mappedY,
|
||||
width: mappedWidth,
|
||||
height: mappedHeight,
|
||||
top: mappedTop,
|
||||
right: mappedRight,
|
||||
bottom: mappedBottom,
|
||||
left: mappedLeft,
|
||||
};
|
||||
};
|
||||
|
||||
const mapPixelFromSmallerToLarger = (
|
||||
onePercentOfSmallerScreen: number,
|
||||
onePercentOfLargerScreen: number,
|
||||
pixel: number
|
||||
): number => {
|
||||
const xPercentOfScreen = pixel / onePercentOfSmallerScreen;
|
||||
return xPercentOfScreen * onePercentOfLargerScreen;
|
||||
};
|
||||
|
||||
const mapPixelFromLargerToSmaller = (
|
||||
onePercentOfSmallerScreen: number,
|
||||
onePercentOfLargerScreen: number,
|
||||
pixel: number
|
||||
): number => {
|
||||
const xPercentOfScreen = pixel / onePercentOfLargerScreen;
|
||||
return Math.round(xPercentOfScreen * onePercentOfSmallerScreen);
|
||||
};
|
||||
127
src/pages/RecordingPage.tsx
Normal file
127
src/pages/RecordingPage.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Grid } from '@mui/material';
|
||||
import { BrowserContent } from "../components/organisms/BrowserContent";
|
||||
import { startRecording, getActiveBrowserId } from "../api/recording";
|
||||
import { LeftSidePanel } from "../components/organisms/LeftSidePanel";
|
||||
import { RightSidePanel } from "../components/organisms/RightSidePanel";
|
||||
import { Loader } from "../components/atoms/Loader";
|
||||
import { useSocketStore } from "../context/socket";
|
||||
import { useBrowserDimensionsStore } from "../context/browserDimensions";
|
||||
import { useGlobalInfoStore } from "../context/globalInfo";
|
||||
import { editRecordingFromStorage } from "../api/storage";
|
||||
import { WhereWhatPair } from "@wbr-project/wbr-interpret";
|
||||
|
||||
interface RecordingPageProps {
|
||||
recordingName?: string;
|
||||
}
|
||||
|
||||
export interface PairForEdit {
|
||||
pair: WhereWhatPair | null,
|
||||
index: number,
|
||||
}
|
||||
|
||||
export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
|
||||
|
||||
const [isLoaded, setIsLoaded] = React.useState(false);
|
||||
const [hasScrollbar, setHasScrollbar] = React.useState(false);
|
||||
const [pairForEdit, setPairForEdit] = useState<PairForEdit>({
|
||||
pair: null,
|
||||
index: 0,
|
||||
});
|
||||
|
||||
const browserContentRef = React.useRef<HTMLDivElement>(null);
|
||||
const workflowListRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const { setId, socket } = useSocketStore();
|
||||
const { setWidth } = useBrowserDimensionsStore();
|
||||
const { browserId, setBrowserId } = useGlobalInfoStore();
|
||||
|
||||
const handleSelectPairForEdit = (pair: WhereWhatPair, index: number) => {
|
||||
setPairForEdit({
|
||||
pair,
|
||||
index,
|
||||
});
|
||||
};
|
||||
|
||||
//resize browser content when loaded event is fired
|
||||
useEffect(() => changeBrowserDimensions(), [isLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
const handleRecording = async () => {
|
||||
const id = await getActiveBrowserId();
|
||||
if (!isCancelled) {
|
||||
if (id) {
|
||||
setId(id);
|
||||
setBrowserId(id);
|
||||
setIsLoaded(true);
|
||||
} else {
|
||||
const newId = await startRecording()
|
||||
setId(newId);
|
||||
setBrowserId(newId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleRecording();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
}
|
||||
}, [setId]);
|
||||
|
||||
const changeBrowserDimensions = useCallback(() => {
|
||||
if (browserContentRef.current) {
|
||||
const currentWidth = Math.floor(browserContentRef.current.getBoundingClientRect().width);
|
||||
const innerHeightWithoutNavBar = window.innerHeight - 54.5;
|
||||
if (innerHeightWithoutNavBar <= (currentWidth / 1.6)) {
|
||||
setWidth(currentWidth - 10);
|
||||
setHasScrollbar(true);
|
||||
} else {
|
||||
setWidth(currentWidth);
|
||||
}
|
||||
socket?.emit("rerender");
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
const handleLoaded = useCallback(() => {
|
||||
if (recordingName && browserId) {
|
||||
editRecordingFromStorage(browserId, recordingName).then(() => setIsLoaded(true));
|
||||
} else {
|
||||
if (browserId === 'new-recording') {
|
||||
socket?.emit('new-recording');
|
||||
}
|
||||
setIsLoaded(true);
|
||||
}
|
||||
}, [socket, browserId, recordingName, isLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
socket?.on('loaded', handleLoaded);
|
||||
return () => {
|
||||
socket?.off('loaded', handleLoaded)
|
||||
}
|
||||
}, [socket, handleLoaded]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoaded ?
|
||||
<Grid container direction="row" spacing={0}>
|
||||
{/* <Grid item xs={2} ref={workflowListRef} style={{ display: "flex", flexDirection: "row" }}>
|
||||
<LeftSidePanel
|
||||
sidePanelRef={workflowListRef.current}
|
||||
alreadyHasScrollbar={hasScrollbar}
|
||||
recordingName={recordingName ? recordingName : ''}
|
||||
handleSelectPairForEdit={handleSelectPairForEdit}
|
||||
/>
|
||||
</Grid> */}
|
||||
<Grid id="browser-content" ref={browserContentRef} item xs>
|
||||
<BrowserContent />
|
||||
</Grid>
|
||||
{/* <Grid item xs={2}>
|
||||
<RightSidePanel pairForEdit={pairForEdit} changeBrowserDimensions={changeBrowserDimensions}/>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
: <Loader />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user