From 793d7e65217a16bc069d84027b8a4b594e72fd16 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Mon, 21 Sep 2020 12:22:19 +0200 Subject: [PATCH] WIP live reloading editor --- client/modules/IDE/components/Editor.jsx | 1 - .../modules/IDE/components/PreviewFrame.jsx | 92 ++++++- client/modules/IDE/pages/IDEView.jsx | 29 ++- client/modules/IDE/reducers/files.js | 18 +- dist/static/assets/webcam.js | 237 ++++++------------ docker-compose.yml | 44 ++-- 6 files changed, 226 insertions(+), 195 deletions(-) diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index e31ff525..d10c7177 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -321,7 +321,6 @@ class Editor extends React.Component { 'editor--options': this.props.editorOptionsVisible }); - console.log(this.props.file); const editorHolderClass = classNames({ 'editor-holder': true, 'editor-holder--hidden': this.props.file.fileType === 'folder' || this.props.file.url diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index ef33bb3f..6398a459 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -23,6 +23,8 @@ import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets } from '../../../utils/consoleUtils'; +// let lastUpdate = null; + const shouldRenderSketch = (props, prevProps = undefined) => { const { isPlaying, previewIsRefreshing, fullView } = props; @@ -327,17 +329,77 @@ class PreviewFrame extends React.Component { renderSketch() { const doc = this.iframeElement; - const localFiles = this.injectLocalFiles(); - if (this.props.isPlaying) { - this.props.clearConsole(); - srcDoc.set(doc, localFiles); - if (this.props.endSketchRefresh) { - this.props.endSketchRefresh(); - } + // const localFiles = this.injectLocalFiles(); + console.log('renderSketch()', this.props); + + const changedFiles = this.props.files.filter(file => file.changed); + const filesRequiringReload = changedFiles.filter(file => !file.content.startsWith('// liveUpdate')); + const filesToHotSwap = changedFiles.filter(file => filesRequiringReload.indexOf(file) === -1); + console.log('changed and requiring reload:', changedFiles, filesRequiringReload, filesToHotSwap); + + let saving; + if (changedFiles.length > 0) { + saving = this.props.saveProject(); } else { - doc.srcdoc = ''; - srcDoc.set(doc, ' '); + // can be done pretier: a promise that always resolves + saving = new Promise((resolve, err) => resolve()); } + + saving.catch(() => { + console.log('Error saving... not authenticated?'); + window.location.href = '/login'; + }); + + saving.then(() => { + if (this.props.isPlaying) { + this.props.clearConsole(); + doc.removeAttribute('srcdoc'); + const source = `${window.location.origin}/${this.props.project.owner.username}/sketches/${this.props.project.id}/index.html`; + // console.log('FILES', this.props.files, doc.src, source, lastUpdate); + if (doc.src === source) { + // const newFiles = this.props.files.filter(file => new Date(file.updatedAt) > lastUpdate); + if (this.props.unsavedChanges) { + // console.log('unsaved changes'); + } + + + console.log('doc', doc); + // we need a hard reload + if (filesRequiringReload.length > 0) { + doc.src = source; // for now... + } else { + // if (doc.contentWindow.document.querySelector('script[src="/assets/hotswap.js"]') == null) { + // const headEl = doc.contentWindow.document.querySelector('head'); + // const srcEl = doc.contentWindow.document.createElement('script'); + // srcEl.src = '/assets/hotswap.js'; + // headEl.appendChild(srcEl); + // } + + console.log('Hot swap (..append):', filesToHotSwap); + const headEl = doc.contentWindow.document.querySelector('head'); + const updatevar = Date.now(); + filesToHotSwap.forEach((file) => { + // doc.contentWindow.postMessage({ 'action': 'code', 'contents': file.content }, '*'); + const srcEl = doc.contentWindow.document.createElement('script'); + srcEl.src = `${file.name}?changed=${updatevar}`; + headEl.appendChild(srcEl); + }); + } + // if ( this.props.htmlFile.content === doc.contentWindow) + // TODO: don't set, but update only (will be hard... :-P) + } else { + doc.src = source; + } + // lastUpdate = new Date(); + if (this.props.endSketchRefresh) { + this.props.endSketchRefresh(); + } + } else { + doc.removeAttribute('src'); + doc.srcdoc = ''; + srcDoc.set(doc, ' '); + } + }); } render() { @@ -384,7 +446,18 @@ PreviewFrame.propTypes = { setBlobUrl: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, expandConsole: PropTypes.func.isRequired, + saveProject: PropTypes.func.isRequired, + project: PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string.isRequired, + owner: PropTypes.shape({ + username: PropTypes.string, + id: PropTypes.string, + }), + updatedAt: PropTypes.string, + }).isRequired, clearConsole: PropTypes.func.isRequired, + unsavedChanges: PropTypes.bool, cmController: PropTypes.shape({ getContent: PropTypes.func }), @@ -392,6 +465,7 @@ PreviewFrame.propTypes = { PreviewFrame.defaultProps = { fullView: false, + unsavedChanges: false, cmController: {} }; diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index c5b358f1..c30cc30f 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -173,6 +173,21 @@ class IDEView extends React.Component { clearTimeout(this.autosaveInterval); this.autosaveInterval = null; } + saveProject() { + // return a Promise to save or null + if ( + isUserOwner(this.props) || + (this.props.user.authenticated && !this.props.project.owner) + ) { + console.log('project to save:', this.props.project); + return this.props.saveProject(this.cmController.getContent()); + } else if (this.props.user.authenticated) { + return this.props.cloneProject(); + } + + this.props.showErrorModal('forceAuthentication'); + return null; + } handleGlobalKeydown(e) { // 83 === s if ( @@ -181,16 +196,7 @@ class IDEView extends React.Component { ) { e.preventDefault(); e.stopPropagation(); - if ( - isUserOwner(this.props) || - (this.props.user.authenticated && !this.props.project.owner) - ) { - this.props.saveProject(this.cmController.getContent()); - } else if (this.props.user.authenticated) { - this.props.cloneProject(); - } else { - this.props.showErrorModal('forceAuthentication'); - } + this.saveProject(); // 13 === enter } else if ( e.keyCode === 13 && @@ -387,8 +393,11 @@ class IDEView extends React.Component { setBlobUrl={this.props.setBlobUrl} expandConsole={this.props.expandConsole} clearConsole={this.props.clearConsole} + saveProject={this.saveProject.bind(this)} + project={this.props.project} cmController={this.cmController} language={this.props.preferences.language} + unsavedChanges={this.props.ide.unsavedChanges} /> diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index 80557fb3..e6f1d35b 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -147,8 +147,21 @@ const files = (state, action) => { return file; } - return Object.assign({}, file, { content: action.content }); + return Object.assign({}, file, { content: action.content, changed: true }); }); + case ActionTypes.SET_UNSAVED_CHANGES: + if (action.value) { + // ignore. + return state; + } + + return state.map((file) => { + if (file.changed) { + return Object.assign({}, file, { changed: false }); + } + return file; + }); + // return Object.assign({}, state, { unsavedChanges: action.value }); case ActionTypes.SET_BLOB_URL: return state.map((file) => { if (file.id !== action.id) { @@ -173,7 +186,8 @@ const files = (state, action) => { content: action.content, url: action.url, children: action.children, - fileType: action.fileType || 'file' + fileType: action.fileType || 'file', + changed: false, }]; return newState.map((file) => { if (file.id === action.parentId) { diff --git a/dist/static/assets/webcam.js b/dist/static/assets/webcam.js index 915db520..3cd3408c 100644 --- a/dist/static/assets/webcam.js +++ b/dist/static/assets/webcam.js @@ -1,110 +1,113 @@ console.log('p5 version:', p5); console.log('ml5 version:', ml5); +console.log(location.origin); let assets = {}; var draw = function () { - // //test - // background(parseInt(Math.random()*255),parseInt(Math.random()*255),parseInt(Math.random()*255)); - // image(video, -width/2, -height/2, width, height); - // console.log(detections) }; -var gotResults = function(err, result) { - if (err) { - console.log(err) - return - } -}; +// var gotResults = function(err, result) { +// if (err) { +// console.log(err) +// return +// } +// }; -function code_error(type, error) { - window.parent.postMessage({ - 'type': type, - 'error': error.message, - 'name': error.name, - 'line': error.lineNumber - 2, // seems it giveswrong line numbers - 'column': error.columnNumber - }, '*'); +// function code_error(type, error) { +// window.parent.postMessage({ +// 'type': type, +// 'error': error.message, +// 'name': error.name, +// 'line': error.lineNumber - 2, // seems it giveswrong line numbers +// 'column': error.columnNumber +// }, '*'); -} +// } -function no_code_error(type){ - window.parent.postMessage({ - 'type': type, - 'error': null - }, '*'); -} +// function no_code_error(type){ +// window.parent.postMessage({ +// 'type': type, +// 'error': null +// }, '*'); +// } -window.addEventListener("message", function (e) { - if (event.origin !== window.location.origin) { - console.error("Invalid origin of message. Ignored"); - return; - } +// window.addEventListener("message", function (e) { +// if (event.origin !== window.location.origin) { +// console.error("Invalid origin of message. Ignored"); +// return; +// } - console.debug("receive", e.data); +// console.debug("receive", e.data); - switch (e.data.action) { - case 'asset': - if(e.data.content === null){ - delete assets[e.data.id]; - } else { - assets[e.data.id] = loadImage(e.data.content); - } +// switch (e.data.action) { +// case 'asset': +// if(e.data.content === null){ +// delete assets[e.data.id]; +// } else { +// assets[e.data.id] = loadImage(e.data.content); +// } - break; - case 'code': - let f = new Function(""); - try { - f = new Function(e.data.draw); - no_code_error('syntax'); - } catch (error) { - code_error('syntax', error); - // window.parent.postMessage({'syntax': error.lineNumber}); - } - handleResults = f; - break; +// break; +// case 'code': +// let f = new Function(""); +// try { +// f = new Function(e.data.draw); +// no_code_error('syntax'); +// } catch (error) { +// code_error('syntax', error); +// // window.parent.postMessage({'syntax': error.lineNumber}); +// } +// handleResults = f; +// break; - default: - console.error("Invalid action", e.data.action); - break; - } +// default: +// console.error("Invalid action", e.data.action); +// break; +// } -}); +// }); let faceapi; var video; -var detections; -var graphics; +var lastFrame; +var detections = []; -let running = true; -function pause() { - if (running) - running = false; - else { - running = true; - faceapi.detect(gotResults); - } -} +// function pause() { +// if (running) +// running = false; +// else { +// running = true; +// faceapi.detect(gotResults); +// } +// } // by default all options are set to true const detection_options = { withLandmarks: true, withDescriptors: false, minConfidence: 0.5, - Mobilenetv1Model: '/assets/faceapi', - FaceLandmarkModel: '/assets/faceapi', - FaceLandmark68TinyNet: '/assets/faceapi', - FaceRecognitionModel: '/assets/faceapi', + Mobilenetv1Model: window.parent.location.origin + '/assets/faceapi', + FaceLandmarkModel: window.parent.location.origin + '/assets/faceapi', + FaceLandmark68TinyNet: window.parent.location.origin + '/assets/faceapi', + FaceRecognitionModel: window.parent.location.origin + '/assets/faceapi', + TinyFaceDetectorModel: window.parent.location.origin + '/assets/faceapi', } + function setup() { - createCanvas(1280,720, WEBGL); + // createCanvas(1280,720, WEBGL); + createCanvas(540,420); smooth(); noFill(); + + push(); + translate(-width/2, -height/2); + let constraints = { video: { width: { min: 720 }, @@ -113,9 +116,9 @@ function setup() { audio: false }; - // graphics = createGraphics(); video = createCapture(constraints); - console.log(video.videoWidth); + lastFrame = createGraphics(video.width, video.height); + console.log(video); // HeadGazeSetup(video); // video.size(width, height); @@ -133,90 +136,20 @@ var handleResults = function(){ background((millis()/100)%255,0,0); image(video, -width/2 + 10, -height/2 + 10, width - 20, height -20); }; + gotResults = function(err, result) { if (err) { console.log(err) return } - // console.log(result) - detections = result; - - try{ - push(); - translate(-width/2, -height/2); - handleResults(); - pop(); - - no_code_error('runtime'); - }catch(error){code_error('runtime', error);} - - // // background(220); - // background(255); - - // push(); - // // with WEBGL, the coordinate system is 0,0 in the center. - // translate(-width / 2, -height / 2, 0); - // image(video, 0, 0, width, height); - - // // image(video, 0,0, width, height) - // if (detections) { - // if (detections.length > 0) { - // // console.log(detections) - // drawBox(detections) - // drawLandmarks(detections) - // for (let detection of detections) { - // let t = HeadGazeDetect(detection); - - // let rot = vecToRotation(t.rotation); - - // document.getElementById('yaw').value = rot[0]; - // document.getElementById('roll').value = rot[1]; - // document.getElementById('pitch').value = rot[2]; - // // let gaze = getMappedVectors() - // // noFill(); - // // stroke(161, 255, 0,100); - // // strokeWeight(2); - // // beginShape(); - // // vertex(gaze[0].x,gaze[0].y); - // // vertex(gaze[1].x,gaze[1].y); - // // endShape(); - // // stroke(255, 255, 0,100); - // // beginShape(); - // // vertex(gaze[0].x,gaze[0].y); - // // vertex(gaze[2].x,gaze[2].y); - // // endShape(); - // // stroke(0, 0, 255,100); - // // beginShape(); - // // vertex(gaze[0].x,gaze[0].y); - // // vertex(gaze[3].x,gaze[3].y); - // // endShape(); - - - // // normalMaterial(); - // push(); - // console.log('translate', t.translation.data64F); - - // // texture(graphics); - // translate(width/2, height/2, 10); - // // plane(70); - // // translate(t.translation.data64F[0], t.translation.data64F[1], t.translation.data64F[2]) - // // rotateX(-rot[2]); - // rotateY(rot[0]); - // // rotateZ(rot[1]); - - // stroke(255, 0, 0); - // // texture(graphics); - // plane(70); - // pop(); - // } - // } - - // } - // pop(); - if (running) - faceapi.detect(gotResults); + // store data for async draw function + detections = result; + lastFrame.background('red'); + lastFrame.image(video, 0,0, video.width, video.height); + + faceapi.detect(gotResults); } function drawBox(detections) { @@ -237,10 +170,6 @@ function drawBox(detections) { } function drawLandmarks(detections) { - // noFill(); - // stroke(161, 95, 251) - // strokeWeight(2) - for (let i = 0; i < detections.length; i++) { const mouth = detections[i].parts.mouth; const nose = detections[i].parts.nose; diff --git a/docker-compose.yml b/docker-compose.yml index 0aa4c915..1cb0c283 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: - dbdata:/data/db app: build: - context: . + context: ./ dockerfile: Dockerfile target: production # uncomment the following line to pull the image from docker hub @@ -17,36 +17,42 @@ services: # - "$PWD/.env.production" environment: - API_URL - - MONGO_URL - - PORT - - SESSION_SECRET - AWS_ACCESS_KEY - - AWS_SECRET_KEY - - S3_BUCKET - AWS_REGION - - GITHUB_ID - - GITHUB_SECRET - - MAILGUN_DOMAIN - - MAILGUN_KEY + - AWS_SECRET_KEY + - CORS_ALLOW_LOCALHOST - EMAIL_SENDER - EMAIL_VERIFY_SECRET_TOKEN - - S3_BUCKET_URL_BASE - - GG_EXAMPLES_USERNAME - - GG_EXAMPLES_PASS - - GG_EXAMPLES_EMAIL - - GOOGLE_ID - - GOOGLE_SECRET + - EXAMPLE_USERNAME - EXAMPLE_USER_EMAIL - EXAMPLE_USER_PASSWORD + - GG_EXAMPLES_USERNAME + - GG_EXAMPLES_EMAIL + - GG_EXAMPLES_PASS - ML5_EXAMPLES_USERNAME - - ML5_EXAMPLES_PASS - ML5_EXAMPLES_EMAIL + - ML5_EXAMPLES_PASS + - GITHUB_ID + - GITHUB_SECRET + - GOOGLE_ID + - GOOGLE_SECRET + - MAILGUN_DOMAIN + - MAILGUN_KEY + - MONGO_URL + - PORT + - S3_BUCKET + - S3_BUCKET_URL_BASE + - SESSION_SECRET + - UI_ACCESS_TOKEN_ENABLED + - UPLOAD_LIMIT + - MOBILE_ENABLED + # you can either set this in your .env or as an environment variables # or here YOU CHOOSE # - MONGO_URL=mongodb://mongo:27017/p5js-web-editor volumes: - - .:/opt/node/app - - /opt/node/app/node_modules + - .:/usr/src/app + - /usr/src/app/node_modules ports: - '8000:8000' depends_on: