diff --git a/src/frontend/apis/index.js b/src/frontend/apis/index.js index 60631b1fa88df708fef57658451b9050f2a3b587..da3aa86f1b5969901d244b38054d6ed49d41a9f2 100644 --- a/src/frontend/apis/index.js +++ b/src/frontend/apis/index.js @@ -70,6 +70,15 @@ const GitHubApi = { let jsWorker = null; const TracerApi = { + md: ({ code }) => Promise.resolve([{ + tracerKey: '0-MarkdownTracer-Markdown', + method: 'construct', + args: ['MarkdownTracer', 'Markdown'], + }, { + tracerKey: '0-MarkdownTracer-Markdown', + method: 'set', + args: [code], + }]), js: ({ code }) => new Promise((resolve, reject) => { if (jsWorker) jsWorker.terminate(); jsWorker = new Worker('/api/tracers/js'); @@ -85,4 +94,4 @@ export { CategoryApi, GitHubApi, TracerApi, -}; \ No newline at end of file +}; diff --git a/src/frontend/common/stylesheet/colors.scss b/src/frontend/common/stylesheet/colors.scss index e829675bdbe885bd5bf79ad6a201e70428f141fb..6882265259d311b37f7b1b012eba9de7128279ff 100644 --- a/src/frontend/common/stylesheet/colors.scss +++ b/src/frontend/common/stylesheet/colors.scss @@ -8,6 +8,7 @@ $color-alert: #f3bd58; $color-selected: #2962ff; $color-patched: #c51162; $color-highlight: #29d; +$color-active: #00e676; :export { themeDark: $theme-dark; @@ -20,4 +21,5 @@ $color-highlight: #29d; colorSelected: $color-selected; colorPatched: $color-patched; colorHighlight: $color-highlight; -} \ No newline at end of file + colorActive: $color-active; +} diff --git a/src/frontend/components/App/index.jsx b/src/frontend/components/App/index.jsx index a50f027b52a30567f4487253bf2f37a8c8ba6c68..44cf4c31d06791583a87e66a5758920e04936d76 100644 --- a/src/frontend/components/App/index.jsx +++ b/src/frontend/components/App/index.jsx @@ -11,7 +11,6 @@ import 'axios-progress-bar/dist/nprogress.css'; import { CodeEditor, Header, - MarkdownViewer, Navigator, ResizableContainer, TabContainer, @@ -19,10 +18,9 @@ import { VisualizationViewer, } from '/components'; import { CategoryApi, GitHubApi } from '/apis'; -import { tracerManager } from '/core'; import { actions } from '/reducers'; import { extension, refineGist } from '/common/util'; -import { languages, exts, us } from '/common/config'; +import { exts, languages, us } from '/common/config'; import { README_MD, SCRATCH_PAPER_MD } from '/skeletons'; import styles from './stylesheet.scss'; @@ -36,7 +34,6 @@ class App extends React.Component { this.state = { navigatorOpened: true, workspaceWeights: [1, 2, 2], - viewerTabIndex: 0, editorTabIndex: -1, }; } @@ -53,15 +50,11 @@ class App extends React.Component { CategoryApi.getCategories() .then(({ categories }) => this.props.setCategories(categories)) .catch(this.props.showErrorToast); - - tracerManager.setOnError(error => this.props.showErrorToast({ name: error.name, message: error.message })); } componentWillUnmount() { delete window.signIn; delete window.signOut; - - tracerManager.setOnError(null); } componentWillReceiveProps(nextProps) { @@ -172,10 +165,6 @@ class App extends React.Component { this.setState({ workspaceWeights }); } - handleChangeViewerTabIndex(viewerTabIndex) { - this.setState({ viewerTabIndex }); - } - handleChangeEditorTabIndex(editorTabIndex) { const { files } = this.props.current; if (editorTabIndex === files.length) this.handleAddFile(); @@ -203,8 +192,11 @@ class App extends React.Component { handleDeleteFile(file) { const { files } = this.props.current; const { editorTabIndex } = this.state; - if (files.indexOf(file) < editorTabIndex) this.handleChangeEditorTabIndex(editorTabIndex - 1); - else this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2)); + if (files.indexOf(file) < editorTabIndex) { + this.handleChangeEditorTabIndex(editorTabIndex - 1); + } else { + this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2)); + } this.props.deleteFile(file); } @@ -220,19 +212,21 @@ class App extends React.Component { serializeFiles(files) === serializeFiles(lastFiles); } + getDescription() { + const { files } = this.props.current; + const readmeFile = files.find(file => file.name === 'README.md'); + if (!readmeFile) return ''; + const groups = /^\s*# .*\n+([^\n]+)/.exec(readmeFile.content); + return groups && groups[1] || ''; + } + render() { - const { navigatorOpened, workspaceWeights, viewerTabIndex, editorTabIndex } = this.state; + const { navigatorOpened, workspaceWeights, editorTabIndex } = this.state; const { titles, files } = this.props.current; const gistSaved = this.isGistSaved(); - const readmeFile = files.find(file => file.name === 'README.md') || { - name: 'README.md', - content: `# ${titles[1]}\nREADME.md not found`, - contributors: [us], - }; - const groups = /^\s*# .*\n+([^\n]+)/.exec(readmeFile.content); - const description = groups && groups[1] || ''; + const description = this.getDescription(); const editorTitles = files.map(file => file.name); if (files[editorTabIndex]) { @@ -253,17 +247,13 @@ class App extends React.Component {
this.toggleNavigatorOpened()} navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()} - loadAlgorithm={params => this.loadAlgorithm(params)} - onAction={() => this.handleChangeViewerTabIndex(1)} gistSaved={gistSaved} /> + loadAlgorithm={params => this.loadAlgorithm(params)} gistSaved={gistSaved} + file={files[editorTabIndex]} /> this.handleChangeWorkspaceWeights(weights)}> this.loadAlgorithm(params)} /> - this.handleChangeViewerTabIndex(tabIndex)}> - - - + this.handleChangeEditorTabIndex(tabIndex)}> { diff --git a/src/frontend/components/App/stylesheet.scss b/src/frontend/components/App/stylesheet.scss index 165d7e949ec26d5bc1c9a1e1a8ab0590fadde1e0..34d3b5a6b27dfd7e0ed71f1d36b533cffff42f6f 100644 --- a/src/frontend/components/App/stylesheet.scss +++ b/src/frontend/components/App/stylesheet.scss @@ -55,6 +55,10 @@ button { .workspace { flex: 1; + .visualization_viewer { + background-color: $theme-dark; + } + .editor_tab_container { .input_title { input { @@ -75,4 +79,4 @@ button { right: 0; z-index: 99; } -} \ No newline at end of file +} diff --git a/src/frontend/components/Button/index.jsx b/src/frontend/components/Button/index.jsx index 1285f047435ddf6552449caed41c9d1fbe33d4a4..d0a03643241305f480c3b911a6e856e12f95b62b 100644 --- a/src/frontend/components/Button/index.jsx +++ b/src/frontend/components/Button/index.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import faExclamationCircle from '@fortawesome/fontawesome-free-solid/faExclamationCircle'; +import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner'; import { classes } from '/common/util'; import { Ellipsis } from '/components'; import styles from './stylesheet.scss'; @@ -22,7 +23,7 @@ class Button extends React.Component { } render() { - let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, ...rest } = this.props; + let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, inProgress, ...rest } = this.props; const { confirming } = this.state; if (confirmNeeded) { @@ -54,7 +55,8 @@ class Button extends React.Component { typeof icon === 'string' ?
: - + ), children, ], diff --git a/src/frontend/components/Button/stylesheet.scss b/src/frontend/components/Button/stylesheet.scss index 1b6cc8533d4e9e53a8393684a3cd33214f718494..a99db8539647b87f3c62ba1f77c2a874b3d71e4f 100644 --- a/src/frontend/components/Button/stylesheet.scss +++ b/src/frontend/components/Button/stylesheet.scss @@ -54,7 +54,7 @@ font-weight: bold; .icon { - color: #00e676; + color: $color-active; } } } @@ -76,4 +76,4 @@ &.confirming { color: $color-alert; } -} \ No newline at end of file +} diff --git a/src/frontend/components/CodeEditor/index.jsx b/src/frontend/components/CodeEditor/index.jsx index 49f8da004e97963def7b48fa89fea46c131ea3de..4568a88a940d393c14c41bf889118a387ec2199d 100644 --- a/src/frontend/components/CodeEditor/index.jsx +++ b/src/frontend/components/CodeEditor/index.jsx @@ -10,7 +10,6 @@ import 'brace/theme/tomorrow_night_eighties'; import 'brace/ext/searchbox'; import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt'; import faUser from '@fortawesome/fontawesome-free-solid/faUser'; -import { tracerManager } from '/core'; import { classes, extension } from '/common/util'; import { actions } from '/reducers'; import { connect } from 'react-redux'; @@ -18,58 +17,22 @@ import { languages } from '/common/config'; import { Button, Ellipsis } from '/components'; import styles from './stylesheet.scss'; -@connect(({ current, env }) => ({ current, env }), actions) +@connect(({ current, env, player }) => ({ current, env, player }), actions) class CodeEditor extends React.Component { - constructor(props) { - super(props); - - this.state = { - lineMarker: null, - }; - } - componentDidMount() { - const { file } = this.props; - tracerManager.setFile(file, true); - - tracerManager.setOnUpdateLineIndicator(lineIndicator => this.setState({ lineMarker: this.createLineMarker(lineIndicator) })); - } - - componentWillReceiveProps(nextProps) { - const { file } = nextProps; - if (file !== this.props.file) { - tracerManager.setFile(file, extension(file.name) === 'js'); - } - } - - componentWillUnmount() { - tracerManager.setOnUpdateLineIndicator(null); - } - - createLineMarker(lineIndicator) { - if (lineIndicator === null) return null; - const { lineNumber, cursor } = lineIndicator; - return { - startRow: lineNumber, - startCol: 0, - endRow: lineNumber, - endCol: Infinity, - className: styles.current_line_marker, - type: 'line', - inFront: true, - _key: cursor, - }; + this.props.shouldBuild(); } handleChangeCode(code) { const { file } = this.props; this.props.modifyFile({ ...file, content: code }); + if (extension(file.name) === 'md') this.props.shouldBuild(); } render() { const { className, file, onDeleteFile } = this.props; const { user } = this.props.env; - const { lineMarker } = this.state; + const { lineIndicator } = this.props.player; const fileExt = extension(file.name); const language = languages.find(language => language.ext === fileExt); @@ -84,7 +47,16 @@ class CodeEditor extends React.Component { name="code_editor" editorProps={{ $blockScrolling: true }} onChange={code => this.handleChangeCode(code)} - markers={lineMarker ? [lineMarker] : []} + markers={lineIndicator ? [{ + startRow: lineIndicator.lineNumber, + startCol: 0, + endRow: lineIndicator.lineNumber, + endCol: Infinity, + className: styles.current_line_marker, + type: 'line', + inFront: true, + _key: lineIndicator.cursor, + }] : []} value={file.content} />
Contributed by diff --git a/src/frontend/components/Header/index.jsx b/src/frontend/components/Header/index.jsx index c122337f260c56685b2d7083ce799662a7b2a28f..4004637fdba106ed62378829bce1fee43edcfc9a 100644 --- a/src/frontend/components/Header/index.jsx +++ b/src/frontend/components/Header/index.jsx @@ -1,6 +1,5 @@ import React from 'react'; import { connect } from 'react-redux'; -import InputRange from 'react-input-range'; import AutosizeInput from 'react-input-autosize'; import screenfull from 'screenfull'; import Promise from 'bluebird'; @@ -8,9 +7,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight'; import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown'; import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight'; -import faPlay from '@fortawesome/fontawesome-free-solid/faPlay'; -import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft'; -import faPause from '@fortawesome/fontawesome-free-solid/faPause'; import faExpandArrowsAlt from '@fortawesome/fontawesome-free-solid/faExpandArrowsAlt'; import faGithub from '@fortawesome/fontawesome-free-brands/faGithub'; import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt'; @@ -21,29 +17,11 @@ import { GitHubApi } from '/apis'; import { classes, refineGist } from '/common/util'; import { actions } from '/reducers'; import { languages } from '/common/config'; -import { Button, Ellipsis, ListItem } from '/components'; -import { tracerManager } from '/core'; +import { Button, Ellipsis, ListItem, Player } from '/components'; import styles from './stylesheet.scss'; @connect(({ current, env }) => ({ current, env }), actions) class Header extends React.Component { - constructor(props) { - super(props); - - const { interval, paused, started } = tracerManager; - this.state = { - interval, paused, started, - }; - } - - componentDidMount() { - tracerManager.setOnUpdateStatus(update => this.setState(update)); - } - - componentWillUnmount() { - tracerManager.setOnUpdateStatus(null); - } - handleClickFullScreen() { if (screenfull.enabled) { if (screenfull.isFullscreen) { @@ -97,12 +75,10 @@ class Header extends React.Component { } render() { - const { interval, paused, started } = this.state; - const { className, onClickTitleBar, navigatorOpened, onAction, gistSaved } = this.props; + const { className, onClickTitleBar, navigatorOpened, gistSaved, file } = this.props; const { gistId, titles } = this.props.current; const { ext, user } = this.props.env; - // TODO: remove the 'run' button and add 'build' and 'play' buttons return (
@@ -159,42 +135,7 @@ class Header extends React.Component {
-
- { - started ? ( - - ) : ( - - ) - } - - { - paused ? ( - - ) : ( - - ) - } - -
- Speed - tracerManager.setInterval(interval)} /> -
-
+
); diff --git a/src/frontend/components/Header/stylesheet.scss b/src/frontend/components/Header/stylesheet.scss index ee52ce3aae0ed74e0473c18c9b67b9f01d532bad..ea55905fcce57e8609d80748928085226b9a30a2 100644 --- a/src/frontend/components/Header/stylesheet.scss +++ b/src/frontend/components/Header/stylesheet.scss @@ -36,51 +36,6 @@ } } - .interval { - display: flex; - align-items: center; - padding: 0 12px; - white-space: nowrap; - - &:hover { - background-color: $color-shadow; - } - - .range { - position: relative; - height: 16px; - width: 60px; - margin-left: 8px; - - .range_label_container { - display: none; - } - - .range_track { - top: 50%; - height: 6px; - margin-top: -3px; - background-color: $theme-light; - cursor: pointer; - display: block; - position: relative; - } - - .range_slider { - top: 0; - width: 6px; - height: 12px; - margin-left: -3px; - margin-top: -3px; - appearance: none; - background-color: $color-font; - cursor: pointer; - display: block; - position: absolute; - } - } - } - .btn_dropdown { position: relative; font-weight: bold; @@ -110,4 +65,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/frontend/components/MarkdownViewer/index.jsx b/src/frontend/components/MarkdownViewer/index.jsx deleted file mode 100644 index f2d2e23298fa37d854165a43ecc6dd8685321bfb..0000000000000000000000000000000000000000 --- a/src/frontend/components/MarkdownViewer/index.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import ReactMarkdown from 'react-markdown' -import { classes } from '/common/util'; -import styles from './stylesheet.scss'; - -class MarkdownViewer extends React.Component { - render() { - const { className, source, onClickLink } = this.props; - - const link = ({ href, ...rest }) => { - return !onClickLink || /^https?:\/\//i.test(href) ? ( - - ) : ( - onClickLink(href)} {...rest} /> - ); - }; - - const image = ({ src, ...rest }) => { - let newSrc; - const codecogs = 'https://latex.codecogs.com/svg.latex?'; - if (src.startsWith(codecogs)) { - const latex = src.substring(codecogs.length); - newSrc = `${codecogs}\\color{White}${latex}`; - } else { - newSrc = src; - } - return - }; - - return ( - - ); - } -} - -export default MarkdownViewer; - diff --git a/src/frontend/components/Player/index.jsx b/src/frontend/components/Player/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..94f3bec509cce2b29fac948c3885d6186d585a76 --- /dev/null +++ b/src/frontend/components/Player/index.jsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Promise from 'bluebird'; +import InputRange from 'react-input-range'; +import faPlay from '@fortawesome/fontawesome-free-solid/faPlay'; +import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft'; +import faChevronRight from '@fortawesome/fontawesome-free-solid/faChevronRight'; +import faPause from '@fortawesome/fontawesome-free-solid/faPause'; +import faWrench from '@fortawesome/fontawesome-free-solid/faWrench'; +import { classes, extension } from '/common/util'; +import { TracerApi } from '/apis'; +import { CompileError } from '/common/error'; +import { actions } from '/reducers'; +import { Button } from '/components'; +import styles from './stylesheet.scss'; +import ProgressBar from '../ProgressBar'; + +@connect(({ player }) => ({ player }), actions) +class Player extends React.Component { + constructor(props) { + super(props); + + this.state = { + interval: 500, + playing: false, + building: false, + }; + + this.reset(); + } + + componentDidMount() { + const { file } = this.props; + this.build(file); + } + + componentWillReceiveProps(nextProps) { + const { file } = nextProps; + const { buildAt } = nextProps.player; + if (buildAt !== this.props.player.buildAt) { + this.build(file); + } + } + + reset(traces = []) { + const chunks = [{ + traces: [], + lineNumber: undefined, + }]; + while (traces.length) { + const trace = traces.shift(); + if (trace.method === 'delay') { + const [lineNumber] = trace.args; + chunks[chunks.length - 1].lineNumber = lineNumber; + chunks.push({ + traces: [], + lineNumber: undefined, + }); + } else { + chunks[chunks.length - 1].traces.push(trace); + } + } + this.props.setChunks(chunks); + this.props.setCursor(0); + this.pause(); + this.props.setLineIndicator(undefined); + } + + build(file) { + if (!file) return; + this.setState({ building: true }); + this.reset(); + const ext = extension(file.name); + (ext in TracerApi ? + TracerApi[ext]({ code: file.content }) : + Promise.reject(new CompileError('Language Not Supported'))) + .then(traces => this.reset(traces)) + .then(() => this.next()) + .catch(error => this.handleError(error)) + .finally(() => this.setState({ building: false })); + } + + isValidCursor(cursor) { + const { chunks } = this.props.player; + return 1 <= cursor && cursor <= chunks.length; + } + + prev() { + this.pause(); + const cursor = this.props.player.cursor - 1; + if (!this.isValidCursor(cursor)) return false; + this.props.setCursor(cursor); + return true; + } + + resume(wrap = false) { + this.pause(); + if (this.next() || wrap && this.props.setCursor(1)) { + this.timer = window.setTimeout(() => this.resume(), this.state.interval); + this.setState({ playing: true }); + } + } + + pause() { + if (this.timer) { + window.clearInterval(this.timer); + this.timer = null; + this.setState({ playing: false }); + } + } + + next() { + this.pause(); + const cursor = this.props.player.cursor + 1; + if (!this.isValidCursor(cursor)) return false; + this.props.setCursor(cursor); + return true; + } + + handleError(error) { + console.error(error); + this.props.showErrorToast({ name: error.name, message: error.message }); + } + + handleChangeInterval(interval) { + this.setState({ interval }); + } + + handleChangeProgress(progress) { + const { chunks } = this.props.player; + const cursor = Math.max(1, Math.min(chunks.length, Math.round(progress * chunks.length))); + this.pause(); + this.props.setCursor(cursor); + } + + render() { + const { className, file } = this.props; + const { chunks, cursor } = this.props.player; + const { interval, playing, building } = this.state; + + return ( +
+ + { + playing ? ( + + ) : ( + + ) + } +
+ ); + } +} + +export default Player; diff --git a/src/frontend/components/Player/stylesheet.scss b/src/frontend/components/Player/stylesheet.scss new file mode 100644 index 0000000000000000000000000000000000000000..8aeba88da4290774b62166c3932517a0400a11f7 --- /dev/null +++ b/src/frontend/components/Player/stylesheet.scss @@ -0,0 +1,52 @@ +@import "~/common/stylesheet/index"; + +.player { + .progress_bar { + width: 160px; + } + + .interval { + display: flex; + align-items: center; + padding: 0 12px; + white-space: nowrap; + + &:hover { + background-color: $color-shadow; + } + + .range { + position: relative; + height: 16px; + width: 60px; + margin-left: 8px; + + .range_label_container { + display: none; + } + + .range_track { + top: 50%; + height: 6px; + margin-top: -3px; + background-color: $theme-light; + cursor: pointer; + display: block; + position: relative; + } + + .range_slider { + top: 0; + width: 6px; + height: 12px; + margin-left: -3px; + margin-top: -3px; + appearance: none; + background-color: $color-font; + cursor: pointer; + display: block; + position: absolute; + } + } + } +} diff --git a/src/frontend/components/ProgressBar/index.jsx b/src/frontend/components/ProgressBar/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..96c9ceb049dfb687d8067e90ba082a789c4776a8 --- /dev/null +++ b/src/frontend/components/ProgressBar/index.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { classes } from '/common/util'; +import styles from './stylesheet.scss'; + +class ProgressBar extends React.Component { + constructor(props) { + super(props); + + this.handleMouseDown = this.handleMouseDown.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); + } + + handleMouseDown(e) { + this.target = e.target; + console.log(this.target) + this.handleMouseMove(e); + document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mouseup', this.handleMouseUp); + } + + handleMouseMove(e) { + const { left } = this.target.getBoundingClientRect(); + const { offsetWidth } = this.target; + const { onChangeProgress } = this.props; + const progress = (e.clientX - left) / offsetWidth; + if (onChangeProgress) onChangeProgress(progress); + } + + handleMouseUp(e) { + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); + } + + render() { + const { className, total, current } = this.props; + + return ( +
+
+
+ {current} / {total} +
+
+ ); + } +} + +export default ProgressBar; diff --git a/src/frontend/components/ProgressBar/stylesheet.scss b/src/frontend/components/ProgressBar/stylesheet.scss new file mode 100644 index 0000000000000000000000000000000000000000..e5663b9a9506cf5d282c49f48c57e9346a9b04d2 --- /dev/null +++ b/src/frontend/components/ProgressBar/stylesheet.scss @@ -0,0 +1,31 @@ +@import "~/common/stylesheet/index"; + +.progress_bar { + display: flex; + align-items: center; + justify-content: center; + position: relative; + background-color: $theme-light; + cursor: pointer; + pointer-events: auto; + + > * { + pointer-events: none; + } + + .active { + position: absolute; + height: 100%; + left: 0; + background-color: $color-active; + } + + .label { + position: absolute; + color: $theme-dark; + + .current { + font-weight: bold; + } + } +} diff --git a/src/frontend/components/VisualizationViewer/index.jsx b/src/frontend/components/VisualizationViewer/index.jsx index 880b141a0ba34ad4511515400e7c75aa53ddb2b2..2669c0c524c7b1a42be50700934d47f05a8f120b 100644 --- a/src/frontend/components/VisualizationViewer/index.jsx +++ b/src/frontend/components/VisualizationViewer/index.jsx @@ -1,44 +1,120 @@ import React from 'react'; +import { connect } from 'react-redux'; import { classes } from '/common/util'; import { ResizableContainer } from '/components'; +import { actions } from '/reducers'; import styles from './stylesheet.scss'; -import { tracerManager } from '../../core'; +import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData, MarkdownData } from '/core/datas'; +@connect(({ player }) => ({ player }), actions) class VisualizationViewer extends React.Component { constructor(props) { super(props); this.state = { - renderers: [], - renderersWeights: [], + dataWeights: {}, }; + + this.datas = []; } componentDidMount() { - // TODO: rendereres should remain resized - tracerManager.setOnChangeRenderers(renderers => { - const renderersWeights = renderers.map(() => 1); - this.setState({ renderers, renderersWeights }); + const { chunks, cursor } = this.props.player; + this.update(chunks, cursor); + } + + componentWillReceiveProps(nextProps) { + const { chunks, cursor } = nextProps.player; + const { chunks: oldChunks, cursor: oldCursor } = this.props.player; + if (chunks !== oldChunks || cursor !== oldCursor) { + this.update(chunks, cursor, oldChunks, oldCursor); + } + } + + update(chunks, cursor, oldChunks = [], oldCursor = 0) { + let applyingChunks; + if (cursor > oldCursor) { + applyingChunks = chunks.slice(oldCursor, cursor); + } else { + this.datas = []; + applyingChunks = chunks.slice(0, cursor); + } + applyingChunks.forEach(chunk => this.applyChunk(chunk)); + + const dataWeights = chunks === oldChunks ? { ...this.state.dataWeights } : {}; + this.datas.forEach(data => { + if (!(data.tracerKey in dataWeights)) { + dataWeights[data.tracerKey] = 1; + } }); + this.setState({ dataWeights }); + + const lastChunk = applyingChunks[applyingChunks.length - 1]; + if (lastChunk && lastChunk.lineNumber !== undefined) { + this.props.setLineIndicator({ lineNumber: lastChunk.lineNumber, cursor }); + } else { + this.props.setLineIndicator(undefined); + } } - componentWillUnmount() { - tracerManager.setOnChangeRenderers(null); + addTracer(className, tracerKey, title) { + const DataClass = { + Tracer: Data, + MarkdownTracer: MarkdownData, + LogTracer: LogData, + Array2DTracer: Array2DData, + Array1DTracer: Array1DData, + ChartTracer: ChartData, + GraphTracer: GraphData, + }[className]; + const data = new DataClass(tracerKey, title, this.datas); + this.datas.push(data); } - handleChangeRenderersWeights(renderersWeights) { - this.setState({ renderersWeights }); + applyTrace(trace) { + const { tracerKey, method, args } = trace; + try { + if (method === 'construct') { + const [className, title] = args; + this.addTracer(className, tracerKey, title); + } else { + const data = this.datas.find(data => data.tracerKey === tracerKey); + data[method](...args); + } + } catch (error) { + this.handleError(error); + } + } + + handleError(error) { + console.error(error); + this.props.showErrorToast({ name: error.name, message: error.message }); + } + + applyChunk(chunk) { + chunk.traces.forEach(trace => this.applyTrace(trace)); + } + + handleChangeWeights(weights) { + const dataWeights = {}; + weights.forEach((weight, i) => { + dataWeights[this.datas[i].tracerKey] = weight; + }); + this.setState({ dataWeights }); } render() { const { className } = this.props; - const { renderers, renderersWeights } = this.state; + const { dataWeights } = this.state; return ( - true)} - onChangeWeights={weights => this.handleChangeRenderersWeights(weights)}> - {renderers} + dataWeights[data.tracerKey])} + visibles={this.datas.map(() => true)} + onChangeWeights={weights => this.handleChangeWeights(weights)}> + { + this.datas.map(data => data.render()) + } ); } diff --git a/src/frontend/components/index.js b/src/frontend/components/index.js index 15ad6535a8b362a03660f7e2c688c0df8ba1e036..23b9849c7555a8e56c39ca33c9e482a0aac150c0 100644 --- a/src/frontend/components/index.js +++ b/src/frontend/components/index.js @@ -6,8 +6,8 @@ export { default as Ellipsis } from './Ellipsis'; export { default as ExpandableListItem } from './ExpandableListItem'; export { default as Header } from './Header'; export { default as ListItem } from './ListItem'; -export { default as MarkdownViewer } from './MarkdownViewer'; export { default as Navigator } from './Navigator'; +export { default as Player } from './Player'; export { default as ResizableContainer } from './ResizableContainer'; export { default as TabContainer } from './TabContainer'; export { default as ToastContainer } from './ToastContainer'; diff --git a/src/frontend/core/datas/Array1DData.js b/src/frontend/core/datas/Array1DData.js index c2fa0b0c75a737d4c09fdaccb63becd72bcd4cf6..5bda332a316121bdca71badc0f512b5a9c3b1628 100644 --- a/src/frontend/core/datas/Array1DData.js +++ b/src/frontend/core/datas/Array1DData.js @@ -1,7 +1,11 @@ import { Array2DData } from '/core/datas'; -import { tracerManager } from '/core'; +import { Array1DRenderer } from '/core/renderers'; class Array1DData extends Array2DData { + getRendererClass() { + return Array1DRenderer; + } + init() { super.init(); this.chartData = null; @@ -30,7 +34,7 @@ class Array1DData extends Array2DData { } chart(tracerKey) { - this.chartData = tracerKey ? tracerManager.datas[tracerKey] : null; + this.chartData = tracerKey ? this.findData(tracerKey) : null; this.syncChartData(); } @@ -39,4 +43,4 @@ class Array1DData extends Array2DData { } } -export default Array1DData; \ No newline at end of file +export default Array1DData; diff --git a/src/frontend/core/datas/Array2DData.js b/src/frontend/core/datas/Array2DData.js index 7f8aa543d8eba621ca2525bf60a695839336466c..416d20cf3809ed2b364a570819b30e5f3cf087e8 100644 --- a/src/frontend/core/datas/Array2DData.js +++ b/src/frontend/core/datas/Array2DData.js @@ -1,6 +1,11 @@ import { Data } from '/core/datas'; +import { Array2DRenderer } from '/core/renderers'; class Array2DData extends Data { + getRendererClass() { + return Array2DRenderer; + } + set(array2d = []) { this.data = []; for (const array1d of array2d) { @@ -60,4 +65,4 @@ class Array2DData extends Data { } } -export default Array2DData; \ No newline at end of file +export default Array2DData; diff --git a/src/frontend/core/datas/ChartData.js b/src/frontend/core/datas/ChartData.js index 5e81b2596e0540e1ef6ed18b7497185c70975ea5..ab8149b312c8bfb824a5f88d835016c72d22bef2 100644 --- a/src/frontend/core/datas/ChartData.js +++ b/src/frontend/core/datas/ChartData.js @@ -1,6 +1,10 @@ import { Array1DData } from '/core/datas'; +import { ChartRenderer } from '/core/renderers'; class ChartData extends Array1DData { + getRendererClass() { + return ChartRenderer; + } } -export default ChartData; \ No newline at end of file +export default ChartData; diff --git a/src/frontend/core/datas/Data.js b/src/frontend/core/datas/Data.js deleted file mode 100644 index 3a5d31a077c5b3412ba1d1b9b45ac26bd3d05c14..0000000000000000000000000000000000000000 --- a/src/frontend/core/datas/Data.js +++ /dev/null @@ -1,30 +0,0 @@ -class Data { - constructor() { - this.init(); - this.reset(); - } - - init() { - } - - setOnRender(onRender) { - this.onRender = onRender; - this.render(); - } - - render() { - if (this.onRender) this.onRender(); - } - - set() { - } - - reset() { - this.set(); - } - - delay() { - } -} - -export default Data; \ No newline at end of file diff --git a/src/frontend/core/datas/Data.jsx b/src/frontend/core/datas/Data.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c090ec017a705600e0833fec5d9fcc314b68e7ff --- /dev/null +++ b/src/frontend/core/datas/Data.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Renderer } from '/core/renderers'; + +class Data { + constructor(tracerKey, title, datas) { + this.tracerKey = tracerKey; + this.title = title; + this.datas = datas; + this.init(); + this.reset(); + } + + findData(tracerKey) { + return this.datas.find(data => data.tracerKey === tracerKey); + } + + getRendererClass() { + return Renderer; + } + + init() { + } + + render() { + const RendererClass = this.getRendererClass(); + return ( + + ); + } + + set() { + } + + reset() { + this.set(); + } +} + +export default Data; diff --git a/src/frontend/core/datas/GraphData.js b/src/frontend/core/datas/GraphData.js index 1f567663e88b46879d18bc180c088b6d78345f69..1f887cec32a8f32307892b4427b090697f98c03f 100644 --- a/src/frontend/core/datas/GraphData.js +++ b/src/frontend/core/datas/GraphData.js @@ -1,8 +1,12 @@ import { Data } from '/core/datas'; import { distance } from '/common/util'; -import { tracerManager } from '/core'; +import { GraphRenderer } from '/core/renderers'; class GraphData extends Data { + getRendererClass() { + return GraphRenderer; + } + init() { super.init(); this.dimensions = { @@ -221,8 +225,8 @@ class GraphData extends Data { } log(tracerKey) { - this.logData = tracerKey ? tracerManager.datas[tracerKey] : null; + this.logData = tracerKey ? this.findData(tracerKey) : null; } } -export default GraphData; \ No newline at end of file +export default GraphData; diff --git a/src/frontend/core/datas/LogData.js b/src/frontend/core/datas/LogData.js index 98bca09392e7f01145253f6476f8d7d5ce13291e..3549ca606a22057ec86cbc87fd3c3fad9815bdc7 100644 --- a/src/frontend/core/datas/LogData.js +++ b/src/frontend/core/datas/LogData.js @@ -1,6 +1,11 @@ import { Data } from '/core/datas'; +import { LogRenderer } from '/core/renderers'; class LogData extends Data { + getRendererClass() { + return LogRenderer; + } + set(messages = []) { this.messages = messages; super.set(); @@ -11,4 +16,4 @@ class LogData extends Data { } } -export default LogData; \ No newline at end of file +export default LogData; diff --git a/src/frontend/core/datas/MarkdownData.js b/src/frontend/core/datas/MarkdownData.js new file mode 100644 index 0000000000000000000000000000000000000000..50ff994aff1172cd8ddb927a1b5e1c9668c822e3 --- /dev/null +++ b/src/frontend/core/datas/MarkdownData.js @@ -0,0 +1,15 @@ +import { Data } from '/core/datas'; +import { MarkdownRenderer } from '/core/renderers'; + +class MarkdownData extends Data { + getRendererClass() { + return MarkdownRenderer; + } + + set(markdown = '') { + this.markdown = markdown; + super.set(); + } +} + +export default MarkdownData; diff --git a/src/frontend/core/datas/index.js b/src/frontend/core/datas/index.js index ea4b89f8ec6992dff63a51df1395f3b17798ce75..e8dc77294bb06bbb104b45cb590edec032562da6 100644 --- a/src/frontend/core/datas/index.js +++ b/src/frontend/core/datas/index.js @@ -1,4 +1,5 @@ export { default as Data } from './Data'; +export { default as MarkdownData } from './MarkdownData'; export { default as LogData } from './LogData'; export { default as Array2DData } from './Array2DData'; export { default as Array1DData } from './Array1DData'; diff --git a/src/frontend/core/index.js b/src/frontend/core/index.js deleted file mode 100644 index 105770ffe0cfa58a307540805bed1afcf811adb1..0000000000000000000000000000000000000000 --- a/src/frontend/core/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as tracerManager } from './tracerManager'; \ No newline at end of file diff --git a/src/frontend/core/renderers/MarkdownRenderer/index.jsx b/src/frontend/core/renderers/MarkdownRenderer/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e8a29c44f4b2b10410b863ba9bf6b4e358760257 --- /dev/null +++ b/src/frontend/core/renderers/MarkdownRenderer/index.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Renderer } from '/core/renderers'; +import styles from './stylesheet.scss'; +import ReactMarkdown from 'react-markdown'; + +class MarkdownRenderer extends Renderer { + renderData() { + const { markdown } = this.props.data; + + const link = ({ href, ...rest }) => { + return ( +
+ ); + }; + + const image = ({ src, ...rest }) => { + let newSrc; + const codecogs = 'https://latex.codecogs.com/svg.latex?'; + if (src.startsWith(codecogs)) { + const latex = src.substring(codecogs.length); + newSrc = `${codecogs}\\color{White}${latex}`; + } else { + newSrc = src; + } + return ; + }; + + return ( + + ); + } +} + +export default MarkdownRenderer; + diff --git a/src/frontend/components/MarkdownViewer/stylesheet.scss b/src/frontend/core/renderers/MarkdownRenderer/stylesheet.scss similarity index 68% rename from src/frontend/components/MarkdownViewer/stylesheet.scss rename to src/frontend/core/renderers/MarkdownRenderer/stylesheet.scss index c8b8a7f974a14a216670023ebc9a56e266e8626b..581639d0a100a4d48591b3b6de6f7c847072fcc8 100644 --- a/src/frontend/components/MarkdownViewer/stylesheet.scss +++ b/src/frontend/core/renderers/MarkdownRenderer/stylesheet.scss @@ -1,15 +1,15 @@ @import "~/common/stylesheet/index"; -.markdown_viewer { +.markdown { display: flex; flex-direction: column; - align-items: stretch; - padding: 16px; + flex: 1; + align-self: stretch; + padding: 24px; font-size: $font-size-large; overflow-y: auto; a { text-decoration: underline; - cursor: pointer; } -} \ No newline at end of file +} diff --git a/src/frontend/core/renderers/Renderer/index.jsx b/src/frontend/core/renderers/Renderer/index.jsx index 6ef937ee73eb2cb54b8e55162a6c5aeb69d906a6..0f56e867316654a44606167be50d76b2d0821fdb 100644 --- a/src/frontend/core/renderers/Renderer/index.jsx +++ b/src/frontend/core/renderers/Renderer/index.jsx @@ -21,39 +21,9 @@ class Renderer extends React.Component { this.zoomMin = 1 / 20; } - componentDidMount() { - const { data } = this.props; - this.mountData(data); - } - - componentWillUnmount() { - const { data } = this.props; - this.unmountData(data); - } - - componentWillReceiveProps(nextProps) { - const { data } = nextProps; - if (data !== this.props.data) { - this.unmountData(this.props.data); - this.mountData(data); - } - } - - shouldComponentUpdate() { - return false; - } - componentDidUpdate(prevProps, prevState, snapshot) { } - mountData(data) { - data.setOnRender(() => this.refresh()); - } - - unmountData(data) { - data.setOnRender(null); - } - handleMouseDown(e) { const { clientX, clientY } = e; this.lastX = clientX; diff --git a/src/frontend/core/renderers/Renderer/stylesheet.scss b/src/frontend/core/renderers/Renderer/stylesheet.scss index 1829dfd43626e37bf6eefd3c828bdcf7d05becca..419e2c91f2cf2bbe754b64253b9227135a4c4fe5 100644 --- a/src/frontend/core/renderers/Renderer/stylesheet.scss +++ b/src/frontend/core/renderers/Renderer/stylesheet.scss @@ -3,7 +3,7 @@ .renderer { position: relative; flex: 1; - flex-direction: column-reverse; + flex-direction: column; display: flex; align-items: center; justify-content: center; @@ -21,4 +21,4 @@ padding: 4px 6px; font-size: $font-size-large; } -} \ No newline at end of file +} diff --git a/src/frontend/core/renderers/index.js b/src/frontend/core/renderers/index.js index 4d2b9f7036bc8a3e41036b073d4106a389102a8f..ead0460455c33807e02eb4b34a3cc8c12b558b42 100644 --- a/src/frontend/core/renderers/index.js +++ b/src/frontend/core/renderers/index.js @@ -1,4 +1,5 @@ export { default as Renderer } from './Renderer'; +export { default as MarkdownRenderer } from './MarkdownRenderer'; export { default as LogRenderer } from './LogRenderer'; export { default as Array2DRenderer } from './Array2DRenderer'; export { default as Array1DRenderer } from './Array1DRenderer'; diff --git a/src/frontend/core/tracerManager.jsx b/src/frontend/core/tracerManager.jsx deleted file mode 100644 index 939b1c70ec978e3345692558e7c77ebe64a0ad3a..0000000000000000000000000000000000000000 --- a/src/frontend/core/tracerManager.jsx +++ /dev/null @@ -1,224 +0,0 @@ -import React from 'react'; -import Promise from 'bluebird'; -import { extension } from '/common/util'; -import { Array1DData, Array2DData, ChartData, Data, GraphData, LogData } from '/core/datas'; -import { Array1DRenderer, Array2DRenderer, ChartRenderer, GraphRenderer, LogRenderer, Renderer } from '/core/renderers'; -import { TracerApi } from '/apis'; -import { CompileError } from '/common/error'; - -class TracerManager { - constructor() { - this.interval = 500; - this.paused = false; - this.started = false; - this.lineIndicator = null; - this.file = { name: '', content: '', contributors: [] }; - this.reset(); - } - - setOnChangeRenderers(onChangeRenderers) { - this.onChangeRenderers = onChangeRenderers; - if (this.onChangeRenderers) this.onChangeRenderers(this.renderers); - } - - setOnUpdateStatus(onUpdateStatus) { - this.onUpdateStatus = onUpdateStatus; - if (this.onUpdateStatus) { - const { interval, paused, started } = this; - this.onUpdateStatus({ interval, paused, started }); - } - } - - setOnUpdateLineIndicator(onUpdateLineIndicator) { - this.onUpdateLineIndicator = onUpdateLineIndicator; - if (this.onUpdateLineIndicator) this.onUpdateLineIndicator(this.lineIndicator); - } - - setOnError(onError) { - this.onError = onError; - } - - render() { - Object.values(this.datas).forEach(data => data.render()); - } - - setInterval(interval) { - this.interval = interval; - if (this.onUpdateStatus) this.onUpdateStatus({ interval }); - } - - setPaused(paused) { - this.paused = paused; - if (this.onUpdateStatus) this.onUpdateStatus({ paused }); - } - - setStarted(started) { - this.started = started; - if (this.onUpdateStatus) this.onUpdateStatus({ started }); - } - - setLineIndicator(lineIndicator) { - this.lineIndicator = lineIndicator; - if (this.onUpdateLineIndicator) this.onUpdateLineIndicator(lineIndicator); - } - - setFile(file, initialRun) { - this.file = file; - if (initialRun) this.runInitial(); - } - - reset(traces = []) { - this.traces = traces; - this.resetCursor(); - this.stopTimer(); - this.setPaused(false); - this.setStarted(false); - this.setLineIndicator(null); - } - - resetCursor() { - this.renderers = []; - this.datas = {}; - this.cursor = 0; - this.chunkCursor = 0; - if (this.onChangeRenderers) this.onChangeRenderers(this.renderers); - } - - addTracer(className, tracerKey, title) { - const [DataClass, RendererClass] = { - Tracer: [Data, Renderer], - LogTracer: [LogData, LogRenderer], - Array2DTracer: [Array2DData, Array2DRenderer], - Array1DTracer: [Array1DData, Array1DRenderer], - ChartTracer: [ChartData, ChartRenderer], - GraphTracer: [GraphData, GraphRenderer], - }[className]; - const data = new DataClass(); - this.datas[tracerKey] = data; - const renderer = ( - - ); - this.renderers.push(renderer); - if (this.onChangeRenderers) this.onChangeRenderers(this.renderers); - } - - applyTrace() { - if (this.cursor >= this.traces.length) return false; - const trace = this.traces[this.cursor++]; - const { tracerKey, method, args } = trace; - try { - if (method === 'construct') { - const [className, title] = args; - this.addTracer(className, tracerKey, title); - return true; - } else { - const data = this.datas[tracerKey]; - const delay = method === 'delay'; - const newArgs = [...args]; - if (delay) { - const lineNumber = newArgs.shift(); - this.setLineIndicator({ lineNumber, cursor: this.cursor }); - } - data[method](...newArgs); - return !delay; - } - } catch (error) { - if (this.started) this.handleError(error); - return false; - } - } - - applyTraceChunk(render = true) { - if (this.cursor >= this.traces.length) return false; - while (this.applyTrace()) ; - this.chunkCursor++; - if (render) this.render(); - return true; - } - - startTimer() { - this.stopTimer(); - if (this.applyTraceChunk()) { - this.timer = window.setTimeout(() => this.startTimer(), this.interval); - } else { - this.setPaused(false); - this.setStarted(false); - } - } - - stopTimer() { - if (this.timer) { - window.clearInterval(this.timer); - this.timer = null; - } - } - - execute() { - const { name, content } = this.file; - const ext = extension(name); - if (ext in TracerApi) { - return TracerApi[ext]({ code: content }); - } else { - return Promise.reject(new CompileError('Language Not Supported')); - } - } - - runInitial() { - this.reset(); - this.render(); - this.execute() - .then(traces => { - this.reset(traces); - this.applyTraceChunk(); - }) - .catch(error => { - }); - } - - run() { - this.reset(); - this.render(); - this.execute() - .then(traces => { - this.reset(traces); - this.resume(); - this.setStarted(true); - }) - .catch(error => { - this.handleError(error); - }); - } - - prev() { - this.pause(); - const lastChunk = this.chunkCursor - 1; - this.resetCursor(); - do { - this.applyTraceChunk(false); - } while (this.chunkCursor < lastChunk); - this.render(); - } - - resume() { - this.startTimer(); - this.setPaused(false); - } - - pause() { - this.stopTimer(); - this.setPaused(true); - } - - next() { - this.pause(); - this.applyTraceChunk(); - } - - handleError(error) { - console.error(error); - if (this.onError) this.onError(error); - } -} - -const tracerManager = new TracerManager(); -export default tracerManager; \ No newline at end of file diff --git a/src/frontend/reducers/index.js b/src/frontend/reducers/index.js index 1f1dee6e4f98845bbbaac7cc81b0544f0ed23a46..c5a7d91d6f269962571cc73c58d30daa9a846515 100644 --- a/src/frontend/reducers/index.js +++ b/src/frontend/reducers/index.js @@ -1,16 +1,19 @@ import { actions as currentActions } from './current'; import { actions as directoryActions } from './directory'; import { actions as envActions } from './env'; +import { actions as playerActions } from './player'; import { actions as toastActions } from './toast'; export { default as current } from './current'; export { default as directory } from './directory'; export { default as env } from './env'; +export { default as player } from './player'; export { default as toast } from './toast'; export const actions = { ...currentActions, ...directoryActions, ...envActions, + ...playerActions, ...toastActions, -}; \ No newline at end of file +}; diff --git a/src/frontend/reducers/player.js b/src/frontend/reducers/player.js new file mode 100644 index 0000000000000000000000000000000000000000..ce37dd1d269dbe96966057857529bc8c7503096d --- /dev/null +++ b/src/frontend/reducers/player.js @@ -0,0 +1,34 @@ +import { combineActions, createAction, handleActions } from 'redux-actions'; + +const prefix = 'PLAYER'; + +const shouldBuild = createAction(`${prefix}/SHOULD_BUILD`, () => ({ buildAt: Date.now() })); +const setChunks = createAction(`${prefix}/SET_CHUNKS`, chunks => ({ chunks })); +const setCursor = createAction(`${prefix}/SET_CURSOR`, cursor => ({ cursor })); +const setLineIndicator = createAction(`${prefix}/SET_LINE_INDICATOR`, lineIndicator => ({ lineIndicator })); + +export const actions = { + shouldBuild, + setChunks, + setCursor, + setLineIndicator, +}; + +const defaultState = { + buildAt: 0, + chunks: [], + cursor: 0, + lineIndicator: undefined, +}; + +export default handleActions({ + [combineActions( + shouldBuild, + setChunks, + setCursor, + setLineIndicator, + )]: (state, { payload }) => ({ + ...state, + ...payload, + }), +}, defaultState);