From d1c9a32458f247959e3cbe939c79fe470d408847 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 12 Jun 2024 01:28:53 +0530 Subject: [PATCH 01/32] chore(deps): install joi --- package.json | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 00000000..5a901005 --- /dev/null +++ b/package.json @@ -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" + } +} From 686353ee60ee3279a8cac93067a895c9aebbd451 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 12 Jun 2024 20:05:43 +0530 Subject: [PATCH 02/32] feat: exports --- mx-interpreter/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 mx-interpreter/index.ts diff --git a/mx-interpreter/index.ts b/mx-interpreter/index.ts new file mode 100644 index 00000000..571f5781 --- /dev/null +++ b/mx-interpreter/index.ts @@ -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'; \ No newline at end of file From 97cf9fbc1ad7cf08b8fb10e659f204ec093d9a26 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 12 Jun 2024 20:06:59 +0530 Subject: [PATCH 03/32] feat: logger --- mx-interpreter/utils/logger.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 mx-interpreter/utils/logger.ts diff --git a/mx-interpreter/utils/logger.ts b/mx-interpreter/utils/logger.ts new file mode 100644 index 00000000..e57421aa --- /dev/null +++ b/mx-interpreter/utils/logger.ts @@ -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 = (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(m); + } else { + process.stdout.write(m); + } + process.stdout.write(`\x1b[${Level.RESET}m\n`); + } \ No newline at end of file From f521ec2c87324b142d60a58c095b9679f3e39b9e Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 12 Jun 2024 20:08:20 +0530 Subject: [PATCH 04/32] feat: array to object utility --- mx-interpreter/utils/utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 mx-interpreter/utils/utils.ts diff --git a/mx-interpreter/utils/utils.ts b/mx-interpreter/utils/utils.ts new file mode 100644 index 00000000..48883dcf --- /dev/null +++ b/mx-interpreter/utils/utils.ts @@ -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]: [] }), {}); + } \ No newline at end of file From 4a1acc9544719238d777351fe566f659b873b762 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 12 Jun 2024 20:09:01 +0530 Subject: [PATCH 05/32] feat: concurrency --- mx-interpreter/utils/concurrency.ts | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 mx-interpreter/utils/concurrency.ts diff --git a/mx-interpreter/utils/concurrency.ts b/mx-interpreter/utils/concurrency.ts new file mode 100644 index 00000000..78ebdf39 --- /dev/null +++ b/mx-interpreter/utils/concurrency.ts @@ -0,0 +1,59 @@ + +export default class Concurrency { + + maxConcurrency : number = 1; + + + activeWorkers : number = 0; + + + private jobQueue : Function[] = []; + + + private waiting : Function[] = []; + + + constructor(maxConcurrency: number) { + this.maxConcurrency = maxConcurrency; + } + + + 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()); + } + } + } + + + addJob(job: () => Promise) : 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!"); + } + } + + + waitForCompletion() : Promise { + return new Promise((res) => { + this.waiting.push(res); + }); + } + } \ No newline at end of file From 779c1f66cc25cb04ee7ecb5a91124e52446d4d32 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 12 Jun 2024 20:09:18 +0530 Subject: [PATCH 06/32] docs: concurrency --- mx-interpreter/utils/concurrency.ts | 44 +++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/mx-interpreter/utils/concurrency.ts b/mx-interpreter/utils/concurrency.ts index 78ebdf39..06ca8a64 100644 --- a/mx-interpreter/utils/concurrency.ts +++ b/mx-interpreter/utils/concurrency.ts @@ -1,23 +1,38 @@ - +/** + * 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(); @@ -37,7 +52,13 @@ export default class Concurrency { } } - + /** + * 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) : void { // console.debug("Adding a worker!"); this.jobQueue.push(job); @@ -50,7 +71,12 @@ export default class Concurrency { } } - + /** + * 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 { return new Promise((res) => { this.waiting.push(res); From 88620344f725476a2ed79c275777b272be84e79f Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Wed, 12 Jun 2024 20:09:31 +0530 Subject: [PATCH 07/32] chore: lint --- mx-interpreter/utils/concurrency.ts | 154 ++++++++++++++-------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/mx-interpreter/utils/concurrency.ts b/mx-interpreter/utils/concurrency.ts index 06ca8a64..eec7eb33 100644 --- a/mx-interpreter/utils/concurrency.ts +++ b/mx-interpreter/utils/concurrency.ts @@ -2,84 +2,84 @@ * 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) : void { - // console.debug("Adding a worker!"); - this.jobQueue.push(job); - - if (!this.maxConcurrency || this.activeWorkers < this.maxConcurrency) { + /** + * 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(); - this.activeWorkers += 1; - } else { - // console.debug("No capacity to run a worker now, waiting!"); + }); + } 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()); } } - - /** - * 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 { - return new Promise((res) => { - this.waiting.push(res); - }); + } + + /** + * 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): 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!"); } - } \ No newline at end of file + } + + /** + * 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 { + return new Promise((res) => { + this.waiting.push(res); + }); + } +} \ No newline at end of file From bd4adba96611fca28988140c5b398967c7b4394a Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 21:35:04 +0530 Subject: [PATCH 08/32] feat: recording --- src/pages/RecordingPage.tsx | 127 ++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/pages/RecordingPage.tsx diff --git a/src/pages/RecordingPage.tsx b/src/pages/RecordingPage.tsx new file mode 100644 index 00000000..cb87f5b0 --- /dev/null +++ b/src/pages/RecordingPage.tsx @@ -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({ + pair: null, + index: 0, + }); + + const browserContentRef = React.useRef(null); + const workflowListRef = React.useRef(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 ( +
+ {isLoaded ? + + {/* + + */} + + + + {/* + + */} + + : } +
+ ); +}; From edce27ee88de71a4a025852c4b80e732e41a0e68 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 21:41:17 +0530 Subject: [PATCH 09/32] feat: browser content --- src/components/organisms/BrowserContent.tsx | 136 ++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/components/organisms/BrowserContent.tsx diff --git a/src/components/organisms/BrowserContent.tsx b/src/components/organisms/BrowserContent.tsx new file mode 100644 index 00000000..189af6ee --- /dev/null +++ b/src/components/organisms/BrowserContent.tsx @@ -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(['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 ( + + + + + + ); +} + +const BrowserContentWrapper = styled.div` + grid-area: browser; +`; From 1f434c952c2d502157282bbed95861aec10085b3 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 21:47:42 +0530 Subject: [PATCH 10/32] feat: browser window --- src/components/organisms/BrowserWindow.tsx | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/components/organisms/BrowserWindow.tsx diff --git a/src/components/organisms/BrowserWindow.tsx b/src/components/organisms/BrowserWindow.tsx new file mode 100644 index 00000000..5a807794 --- /dev/null +++ b/src/components/organisms/BrowserWindow.tsx @@ -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 | undefined>(undefined); + const [screenShot, setScreenShot] = useState(""); + 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 } + + + ); +}; + +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); + }; + +}; From 41736c3ac4aea2f9f2bef2cd09a89728d2e6feee Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 21:48:23 +0530 Subject: [PATCH 11/32] feat: element highlighter --- src/components/atoms/Highlighter.tsx | 127 +++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/components/atoms/Highlighter.tsx diff --git a/src/components/atoms/Highlighter.tsx b/src/components/atoms/Highlighter.tsx new file mode 100644 index 00000000..50e28ec5 --- /dev/null +++ b/src/components/atoms/Highlighter.tsx @@ -0,0 +1,127 @@ +import React from 'react'; + +import styled from "styled-components"; +import { mapRect } from "../../helpers/inputHelpers"; +import canvas from "./canvas"; + +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 ( +
+ + + {displayedSelector} + +
+ ); + } +} + +const HighlighterOutline = styled.div` + 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` + 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; +} From d3c4fc8d0de96e70a843141d6d003d90716eca19 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 21:56:50 +0530 Subject: [PATCH 12/32] feat: canvas --- src/components/atoms/canvas.tsx | 148 ++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/components/atoms/canvas.tsx diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx new file mode 100644 index 00000000..33f45698 --- /dev/null +++ b/src/components/atoms/canvas.tsx @@ -0,0 +1,148 @@ +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): 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(null); + const { socket } = useSocketStore(); + const { setLastAction, lastAction } = useGlobalInfoStore(); + + const notifyLastAction = (action: string) => { + if (lastAction !== action) { + setLastAction(action); + } + }; + + const lastMousePosition = useRef({ x: 0, y: 0 }); + //const lastWheelPosition = useRef({ 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', { + x: event.clientX, + y: event.clientY, + }); + 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 ( + // + + ); + +}; + + +export default Canvas; From c96ad7bc4c5cf43856f828b3ed0541a3eb939cc5 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:14:52 +0530 Subject: [PATCH 13/32] feat: use getMappedCoordinates for coordinates --- src/components/atoms/canvas.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 33f45698..6a2f9dea 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -50,11 +50,8 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { } switch (event.type) { case 'mousedown': - // const clickCoordinates = getMappedCoordinates(event, canvasRef.current, width, height); - socket.emit('input:mousedown', { - x: event.clientX, - y: event.clientY, - }); + const clickCoordinates = getMappedCoordinates(event, canvasRef.current, width, height); + socket.emit('input:mousedown', clickCoordinates); notifyLastAction('click'); break; case 'mousemove': From cdc96e5ef1265b35441f5dcf59e8e93d4f1a304f Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:28:29 +0530 Subject: [PATCH 14/32] feat: throttle helper --- src/helpers/inputHelpers.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/helpers/inputHelpers.ts diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts new file mode 100644 index 00000000..0d395ceb --- /dev/null +++ b/src/helpers/inputHelpers.ts @@ -0,0 +1,21 @@ +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); + } + } +} + From 226001d5a74d2d8d0bdcd0d5413ea7aba5d100c7 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:28:56 +0530 Subject: [PATCH 15/32] feat: getMappedCoordinates --- src/helpers/inputHelpers.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts index 0d395ceb..ed48432c 100644 --- a/src/helpers/inputHelpers.ts +++ b/src/helpers/inputHelpers.ts @@ -19,3 +19,27 @@ export const throttle = (callback: any, limit: number) => { } } +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 + }; +}; + From 41459ed7772c9fcfdd38873d2ad08325557cacd0 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:29:16 +0530 Subject: [PATCH 16/32] feat: getCoordinates --- src/helpers/inputHelpers.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts index ed48432c..0d3d39b0 100644 --- a/src/helpers/inputHelpers.ts +++ b/src/helpers/inputHelpers.ts @@ -43,3 +43,13 @@ export const getMappedCoordinates = ( }; }; +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 + }; +}; + From d832797fdfa644fcb8c8baf7c1fa19159e1c4ed5 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:29:41 +0530 Subject: [PATCH 17/32] feat: map rect --- src/helpers/inputHelpers.ts | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts index 0d3d39b0..9fb3b5ca 100644 --- a/src/helpers/inputHelpers.ts +++ b/src/helpers/inputHelpers.ts @@ -53,3 +53,73 @@ const getCoordinates = (event: MouseEvent, canvas: HTMLCanvasElement | null): Co }; }; +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, + }; +}; + + From 51dccba2301943d4ffc42b6a073ed1ebbcd3276c Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:29:58 +0530 Subject: [PATCH 18/32] feat: map from small to large --- src/helpers/inputHelpers.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts index 9fb3b5ca..e3884aa7 100644 --- a/src/helpers/inputHelpers.ts +++ b/src/helpers/inputHelpers.ts @@ -122,4 +122,13 @@ export const mapRect = ( }; }; +const mapPixelFromSmallerToLarger = ( + onePercentOfSmallerScreen: number, + onePercentOfLargerScreen: number, + pixel: number +) : number => { + const xPercentOfScreen = pixel / onePercentOfSmallerScreen; + return xPercentOfScreen * onePercentOfLargerScreen; +}; + From 342e131fed7b6419a7f9c41b119494a91d192e79 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:30:43 +0530 Subject: [PATCH 19/32] feat: map from large to small --- src/helpers/inputHelpers.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts index e3884aa7..928862c5 100644 --- a/src/helpers/inputHelpers.ts +++ b/src/helpers/inputHelpers.ts @@ -131,4 +131,11 @@ const mapPixelFromSmallerToLarger = ( return xPercentOfScreen * onePercentOfLargerScreen; }; - +const mapPixelFromLargerToSmaller = ( + onePercentOfSmallerScreen: number, + onePercentOfLargerScreen: number, + pixel: number +) : number => { + const xPercentOfScreen = pixel / onePercentOfLargerScreen; + return Math.round(xPercentOfScreen * onePercentOfSmallerScreen); +}; From b34410b5c036ff64aa1c4dbd9e8b9252dce5cb9c Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:31:05 +0530 Subject: [PATCH 20/32] chore: lint --- src/helpers/inputHelpers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/inputHelpers.ts b/src/helpers/inputHelpers.ts index 928862c5..d7861734 100644 --- a/src/helpers/inputHelpers.ts +++ b/src/helpers/inputHelpers.ts @@ -45,7 +45,7 @@ export const getMappedCoordinates = ( const getCoordinates = (event: MouseEvent, canvas: HTMLCanvasElement | null): Coordinates => { if (!canvas) { - return { x: 0, y: 0}; + return { x: 0, y: 0 }; } return { x: event.pageX - canvas.offsetLeft, @@ -126,7 +126,7 @@ const mapPixelFromSmallerToLarger = ( onePercentOfSmallerScreen: number, onePercentOfLargerScreen: number, pixel: number -) : number => { +): number => { const xPercentOfScreen = pixel / onePercentOfSmallerScreen; return xPercentOfScreen * onePercentOfLargerScreen; }; @@ -135,7 +135,7 @@ const mapPixelFromLargerToSmaller = ( onePercentOfSmallerScreen: number, onePercentOfLargerScreen: number, pixel: number -) : number => { +): number => { const xPercentOfScreen = pixel / onePercentOfLargerScreen; return Math.round(xPercentOfScreen * onePercentOfSmallerScreen); }; From 7ef90f994b387363e02e95cc58586efa3a3bebb9 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:37:41 +0530 Subject: [PATCH 21/32] feat: browser dimension context --- src/context/browserDimensions.tsx | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/context/browserDimensions.tsx diff --git a/src/context/browserDimensions.tsx b/src/context/browserDimensions.tsx new file mode 100644 index 00000000..e1d81681 --- /dev/null +++ b/src/context/browserDimensions.tsx @@ -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{ + width: number = 1280; + height: number = 720; +}; + +const browserDimensionsStore = new BrowserDimensionsStore(); +const browserDimensionsContext = createContext(browserDimensionsStore as BrowserDimensions); + +export const useBrowserDimensionsStore = () => useContext(browserDimensionsContext); + +export const BrowserDimensionsProvider = ({ children }: { children: JSX.Element }) => { + const [width, setWidth] = useState(browserDimensionsStore.width); + const [height, setHeight] = useState(browserDimensionsStore.height); + + const setNewWidth = useCallback((newWidth: number) => { + setWidth(newWidth); + setHeight(Math.round(newWidth / 1.6)); + }, [setWidth, setHeight]); + + return ( + + {children} + + ); +}; From 4642dbdace6eb16eb96e4d58f5291dc22ad285f2 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 22:39:02 +0530 Subject: [PATCH 22/32] feat: browser nav (input) --- src/components/molecules/BrowserNavBar.tsx | 123 +++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/components/molecules/BrowserNavBar.tsx diff --git a/src/components/molecules/BrowserNavBar.tsx b/src/components/molecules/BrowserNavBar.tsx new file mode 100644 index 00000000..e57e7c21 --- /dev/null +++ b/src/components/molecules/BrowserNavBar.tsx @@ -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 = ({ + browserWidth, + handleUrlChanged, +}) => { + + // context: + const { socket } = useSocketStore(); + + const [currentUrl, setCurrentUrl] = useState('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 ( + + { + socket?.emit('input:back'); + }} + disabled={false} + > + + + + { + socket?.emit('input:forward'); + }} + disabled={false} + > + + + + { + if (socket) { + handleRefresh() + } + }} + disabled={false} + > + + + + + + ); +} + +export default BrowserNavBar; From 5f48ac71c9a6e86249bd87d3370c7280ac414e7c Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:08:06 +0530 Subject: [PATCH 23/32] feat: url form for browser nav --- src/components/molecules/UrlForm.tsx | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/components/molecules/UrlForm.tsx diff --git a/src/components/molecules/UrlForm.tsx b/src/components/molecules/UrlForm.tsx new file mode 100644 index 00000000..77e1139a --- /dev/null +++ b/src/components/molecules/UrlForm.tsx @@ -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(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 ( + + + + + + + ); +}; From 63c2c815b3d276b0be02779c33be6ac6146f3791 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:08:31 +0530 Subject: [PATCH 24/32] chore: lint --- src/components/molecules/UrlForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/molecules/UrlForm.tsx b/src/components/molecules/UrlForm.tsx index 77e1139a..a6681ab9 100644 --- a/src/components/molecules/UrlForm.tsx +++ b/src/components/molecules/UrlForm.tsx @@ -1,11 +1,11 @@ -import {useState, useCallback, useEffect,} from 'react'; +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"; +import { Socket } from "socket.io-client"; type Props = { currentAddress: string; @@ -66,7 +66,7 @@ export const UrlForm = ({ onChange={onChange} /> - + ); From 374cf4036f7a10ce403b2c71b858ccf632972046 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:12:31 +0530 Subject: [PATCH 25/32] feat: url & navbar buttons --- src/components/atoms/buttons/buttons.tsx | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/components/atoms/buttons/buttons.tsx diff --git a/src/components/atoms/buttons/buttons.tsx b/src/components/atoms/buttons/buttons.tsx new file mode 100644 index 00000000..506e0de0 --- /dev/null +++ b/src/components/atoms/buttons/buttons.tsx @@ -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; + }, +`; From ea37a2be42a518d733db34aaa4b156e7c7302a49 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:13:39 +0530 Subject: [PATCH 26/32] feat: ui components for nav url form --- src/components/atoms/form.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/components/atoms/form.tsx diff --git a/src/components/atoms/form.tsx b/src/components/atoms/form.tsx new file mode 100644 index 00000000..56a105b3 --- /dev/null +++ b/src/components/atoms/form.tsx @@ -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; +`; From c5823bffd314c8b74bea947e2617c353e507c799 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:16:28 +0530 Subject: [PATCH 27/32] feat: browser tabs --- src/components/molecules/BrowserTabs.tsx | 91 ++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/components/molecules/BrowserTabs.tsx diff --git a/src/components/molecules/BrowserTabs.tsx b/src/components/molecules/BrowserTabs.tsx new file mode 100644 index 00000000..8e2a6a9f --- /dev/null +++ b/src/components/molecules/BrowserTabs.tsx @@ -0,0 +1,91 @@ +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 ( + + + + {tabs.map((tab, index) => { + return ( + { + tabWasClosed = true; + handleCloseTab(index); + }} disabled={tabs.length === 1} + />} + iconPosition="end" + onClick={() => { + if (!tabWasClosed) { + handleTabChange(index) + } + } + } + label={tab} + /> + ); + })} + + + + + ); +} + +interface CloseButtonProps { + closeTab: () => void; + disabled: boolean; +} + +const CloseButton = ({ closeTab, disabled }: CloseButtonProps) => { + return ( + + + + ); +} From 165de974465104e2380986a6af814b389ad37fc3 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:16:50 +0530 Subject: [PATCH 28/32] chore: lint --- src/components/molecules/BrowserTabs.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/molecules/BrowserTabs.tsx b/src/components/molecules/BrowserTabs.tsx index 8e2a6a9f..2cda3937 100644 --- a/src/components/molecules/BrowserTabs.tsx +++ b/src/components/molecules/BrowserTabs.tsx @@ -64,7 +64,7 @@ export const BrowserTabs = ( })} - + ); } @@ -81,11 +81,13 @@ const CloseButton = ({ closeTab, disabled }: CloseButtonProps) => { size={"small"} onClick={closeTab} disabled={disabled} - sx={{ height: '34px', - '&:hover': { color: 'white', backgroundColor: '#1976d2' } }} + sx={{ + height: '34px', + '&:hover': { color: 'white', backgroundColor: '#1976d2' } + }} component="span" > - + ); } From c444cfc00fe9759051da91cac2c6d1a845ca478a Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:17:32 +0530 Subject: [PATCH 29/32] chore: lint --- src/components/organisms/BrowserWindow.tsx | 52 +++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/components/organisms/BrowserWindow.tsx b/src/components/organisms/BrowserWindow.tsx index 5a807794..32ab9607 100644 --- a/src/components/organisms/BrowserWindow.tsx +++ b/src/components/organisms/BrowserWindow.tsx @@ -8,23 +8,23 @@ export const BrowserWindow = () => { const [canvasRef, setCanvasReference] = useState | undefined>(undefined); const [screenShot, setScreenShot] = useState(""); - const [highlighterData, setHighlighterData] = useState<{rect: DOMRect, selector: string} | null>(null); + 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) =>{ + 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 - ){ + e.pageX < canvasRect.left + || e.pageX > canvasRect.right + || e.pageY < canvasRect.top + || e.pageY > canvasRect.bottom + ) { setHighlighterData(null); } } @@ -34,7 +34,7 @@ export const BrowserWindow = () => { setScreenShot(data); }, [screenShot]); - useEffect(() => { + useEffect(() => { if (socket) { socket.on("screencast", screencastHandler); } @@ -50,12 +50,12 @@ export const BrowserWindow = () => { }, [screenShot, canvasRef, socket, screencastHandler]); - const highlighterHandler = useCallback((data: {rect: DOMRect, selector: string}) => { + const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string }) => { setHighlighterData(data); console.log('Highlighter Rect via socket:', data.rect) }, [highlighterData]) - useEffect(() => { + useEffect(() => { document.addEventListener('mousemove', onMouseMove, false); if (socket) { socket.on("highlighter", highlighterHandler); @@ -68,26 +68,26 @@ export const BrowserWindow = () => { }, [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()} + <> + {(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} + - : null } - - + ); }; -const drawImage = (image: string, canvas: HTMLCanvasElement) :void => { +const drawImage = (image: string, canvas: HTMLCanvasElement): void => { const ctx = canvas.getContext('2d'); From 1bcebf99530f3d81d705be26c1e28ff2da3b114e Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:18:05 +0530 Subject: [PATCH 30/32] chore: lint --- src/pages/RecordingPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/RecordingPage.tsx b/src/pages/RecordingPage.tsx index cb87f5b0..7a2f03a8 100644 --- a/src/pages/RecordingPage.tsx +++ b/src/pages/RecordingPage.tsx @@ -44,7 +44,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => { }; //resize browser content when loaded event is fired - useEffect(() => changeBrowserDimensions(), [isLoaded]) + useEffect(() => changeBrowserDimensions(), [isLoaded]) useEffect(() => { let isCancelled = false; @@ -74,7 +74,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => { if (browserContentRef.current) { const currentWidth = Math.floor(browserContentRef.current.getBoundingClientRect().width); const innerHeightWithoutNavBar = window.innerHeight - 54.5; - if ( innerHeightWithoutNavBar <= (currentWidth / 1.6)) { + if (innerHeightWithoutNavBar <= (currentWidth / 1.6)) { setWidth(currentWidth - 10); setHasScrollbar(true); } else { @@ -115,13 +115,13 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => { /> */} - + {/* */} - : } + : } ); }; From e3a67ffbde00efd0debd33427f63c52ec4d80249 Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:18:49 +0530 Subject: [PATCH 31/32] chore: remove unused import --- src/components/atoms/Highlighter.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/atoms/Highlighter.tsx b/src/components/atoms/Highlighter.tsx index 50e28ec5..7e5fb8d7 100644 --- a/src/components/atoms/Highlighter.tsx +++ b/src/components/atoms/Highlighter.tsx @@ -2,7 +2,6 @@ import React from 'react'; import styled from "styled-components"; import { mapRect } from "../../helpers/inputHelpers"; -import canvas from "./canvas"; interface HighlighterProps { unmodifiedRect: DOMRect; From 6e2d7775f3ac50360867fec33af4b4c0a8003ada Mon Sep 17 00:00:00 2001 From: karishmas6 Date: Fri, 14 Jun 2024 23:19:35 +0530 Subject: [PATCH 32/32] refactor: remove mapRect usage --- src/components/atoms/Highlighter.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/atoms/Highlighter.tsx b/src/components/atoms/Highlighter.tsx index 7e5fb8d7..b72cac06 100644 --- a/src/components/atoms/Highlighter.tsx +++ b/src/components/atoms/Highlighter.tsx @@ -1,7 +1,6 @@ -import React from 'react'; +import React from 'react'; import styled from "styled-components"; -import { mapRect } from "../../helpers/inputHelpers"; interface HighlighterProps { unmodifiedRect: DOMRect; @@ -11,7 +10,7 @@ interface HighlighterProps { canvasRect: DOMRect; }; -export const Highlighter = ({ unmodifiedRect, displayedSelector = '', width, height, canvasRect}: HighlighterProps) => { +export const Highlighter = ({ unmodifiedRect, displayedSelector = '', width, height, canvasRect }: HighlighterProps) => { if (!unmodifiedRect) { return null; } else { @@ -30,7 +29,7 @@ export const Highlighter = ({ unmodifiedRect, displayedSelector = '', width, hei const rect = { top: unmodifiedRect.top + canvasRect.top, - left: unmodifiedRect.left + canvasRect.left, + left: unmodifiedRect.left + canvasRect.left, right: unmodifiedRect.right + canvasRect.left, bottom: unmodifiedRect.bottom + canvasRect.top, width: unmodifiedRect.width, @@ -113,7 +112,7 @@ const HighlighterLabel = styled.div` left: ${(p: HighlighterLabelProps) => p.left}px; `; -interface HighlighterLabelProps{ +interface HighlighterLabelProps { top: number; left: number; }