diff --git a/package.json b/package.json index a58e55026ce6de481ac05f110b597435926d7c32..b8649d8c1e8dd0e430e9eae5c1534f69007606bf 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "scratch-audio": "0.1.0-prerelease.1516198804", "scratch-blocks": "0.1.0-prerelease.1519256473", "scratch-l10n": "2.0.20180108132626", - "scratch-paint": "0.2.0-prerelease.20180227161624", + "scratch-paint": "0.2.0-prerelease.20180302160120", "scratch-render": "0.1.0-prerelease.1516837442", "scratch-storage": "0.4.0", "scratch-vm": "0.1.0-prerelease.1519681201-prerelease.1519681262", diff --git a/src/components/crash-message/crash-message.css b/src/components/crash-message/crash-message.css new file mode 100644 index 0000000000000000000000000000000000000000..605f7fdab9e745a602bdb1d9ed32aa123de1b819 --- /dev/null +++ b/src/components/crash-message/crash-message.css @@ -0,0 +1,28 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; + +.crash-wrapper { + background-color: $motion-primary; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} +.body { + width: 35%; + color: white; + text-align: center; +} + +.reloadButton { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + padding: 0.5rem 2rem; + background: white; + color: $motion-primary; + font-weight: bold; + font-size: 0.875rem; + cursor: pointer; +} diff --git a/src/components/crash-message/crash-message.jsx b/src/components/crash-message/crash-message.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d5fc45d163072d6394c5da665ee6427b6d0159a4 --- /dev/null +++ b/src/components/crash-message/crash-message.jsx @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Box from '../box/box.jsx'; + +import styles from './crash-message.css'; +import reloadIcon from './reload.svg'; + +const CrashMessage = props => ( + <div className={styles.crashWrapper}> + <Box className={styles.body}> + <img + className={styles.reloadIcon} + src={reloadIcon} + /> + <h2> + Oops! Something went wrong. + </h2> + <p> + We are so sorry, but it looks like Scratch has crashed. This bug has been + automatically reported to the Scratch Team. Please refresh your page to try + again. + + </p> + <button + className={styles.reloadButton} + onClick={props.onReload} + > + Reload + </button> + </Box> + </div> +); + +CrashMessage.propTypes = { + onReload: PropTypes.func.isRequired +}; + +export default CrashMessage; diff --git a/src/components/crash-message/reload.svg b/src/components/crash-message/reload.svg new file mode 100644 index 0000000000000000000000000000000000000000..b74601fac9d4aaaaf53b50a26411b86949c2e5f3 Binary files /dev/null and b/src/components/crash-message/reload.svg differ diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 5c7a378dc43c44ff9f3d5d37e3b9ae79ffbbe87f..b7f2a0acd1d73abf9f683dd8c7d3505394c8a441 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -14,7 +14,7 @@ import TargetPane from '../../containers/target-pane.jsx'; import SoundTab from '../../containers/sound-tab.jsx'; import StageHeader from '../../containers/stage-header.jsx'; import Stage from '../../containers/stage.jsx'; - +import Loader from '../loader/loader.jsx'; import Box from '../box/box.jsx'; import FeedbackForm from '../feedback-form/feedback-form.jsx'; import MenuBar from '../menu-bar/menu-bar.jsx'; @@ -44,6 +44,7 @@ const GUIComponent = props => { feedbackFormVisible, importInfoVisible, intl, + loading, onExtensionButtonClick, onActivateTab, previewInfoVisible, @@ -78,6 +79,9 @@ const GUIComponent = props => { {previewInfoVisible ? ( <PreviewModal /> ) : null} + {loading ? ( + <Loader /> + ) : null} {importInfoVisible ? ( <ImportModal /> ) : null} @@ -176,6 +180,7 @@ GUIComponent.propTypes = { feedbackFormVisible: PropTypes.bool, importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, + loading: PropTypes.bool, onActivateTab: PropTypes.func, onExtensionButtonClick: PropTypes.func, onTabSelect: PropTypes.func, diff --git a/src/components/loader/bottom-block.svg b/src/components/loader/bottom-block.svg new file mode 100644 index 0000000000000000000000000000000000000000..b397068c448eeabc04d4245e1f5cdd210b72df5c Binary files /dev/null and b/src/components/loader/bottom-block.svg differ diff --git a/src/components/loader/loader.css b/src/components/loader/loader.css new file mode 100644 index 0000000000000000000000000000000000000000..68f8e5f4f08f852f5f1ac3c968d30e5c33729a8e --- /dev/null +++ b/src/components/loader/loader.css @@ -0,0 +1,89 @@ +@import "../../css/colors.css"; + +.background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; /* Below preview modal */ + display: flex; + justify-content: center; + align-items: center; + background-color: $motion-primary; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + text-align: center; + color: white; +} + +.block-animation { + width: 125px; + height: 150px; + margin: 50px auto 0px; +} + +.block-animation img { + display: block; + position: relative; + height: 30%; + margin-top: -4px; +} + +.topBlock { + animation: top-slide-in 1.5s ease infinite; +} + +.middleBlock { + animation: middle-slide-in 1.5s ease infinite; +} + +.bottomBlock { + animation: bottom-slide-in 1.5s ease infinite; +} + + +@keyframes top-slide-in { + 0% { + transform: translateY(50px); + opacity: 0; + } + + 33% { + transform: translateY(0px); + opacity: 1; + } +} + +@keyframes middle-slide-in { + 0% { + transform: translateY(50px); + opacity: 0; + } + + 33% { + transform: translateY(50px); + opacity: 0; + } + + 66% { + transform: translateY(0px); + opacity: 1; + } +} + +@keyframes bottom-slide-in { + 0% { + transform: translateY(50px); + opacity: 0; + } + + 66% { + transform: translateY(50px); + opacity: 0; + } + + 100% { + transform: translateY(0px); + opacity: 1; + } +} diff --git a/src/components/loader/loader.jsx b/src/components/loader/loader.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8a93e5c36abd385665c9fcdd1374f30096d9c0a8 --- /dev/null +++ b/src/components/loader/loader.jsx @@ -0,0 +1,144 @@ +import React from 'react'; +import {FormattedMessage} from 'react-intl'; +import styles from './loader.css'; + +import topBlock from './top-block.svg'; +import middleBlock from './middle-block.svg'; +import bottomBlock from './bottom-block.svg'; + +const LoaderComponent = () => { + const messages = [ + { + message: ( + <FormattedMessage + defaultMessage="Creating blocks …" + description="One of the loading messages" + id="gui.loader.message1" + /> + ), + weight: 50 + }, + { + message: ( + <FormattedMessage + defaultMessage="Loading sprites …" + description="One of the loading messages" + id="gui.loader.message2" + /> + ), + weight: 50 + }, + { + message: ( + <FormattedMessage + defaultMessage="Loading sounds …" + description="One of the loading messages" + id="gui.loader.message3" + /> + ), + weight: 50 + }, + { + message: ( + <FormattedMessage + defaultMessage="Loading extensions …" + description="One of the loading messages" + id="gui.loader.message4" + /> + ), + weight: 50 + }, + { + message: ( + <FormattedMessage + defaultMessage="Creating blocks …" + description="One of the loading messages" + id="gui.loader.message1" + /> + ), + weight: 20 + }, + { + message: ( + <FormattedMessage + defaultMessage="Herding cats …" + description="One of the loading messages" + id="gui.loader.message5" + /> + ), + weight: 1 + }, + { + message: ( + <FormattedMessage + defaultMessage="Transmitting nanos …" + description="One of the loading messages" + id="gui.loader.message6" + /> + ), + weight: 1 + }, + { + message: ( + <FormattedMessage + defaultMessage="Inflating gobos …" + description="One of the loading messages" + id="gui.loader.message7" + /> + ), + weight: 1 + }, + { + message: ( + <FormattedMessage + defaultMessage="Preparing emojiis …" + description="One of the loading messages" + id="gui.loader.message8" + /> + ), + weight: 1 + } + ]; + + let message; + const sum = messages.reduce((acc, m) => acc + m.weight, 0); + let rand = sum * Math.random(); + for (let i = 0; i < messages.length; i++) { + rand -= messages[i].weight; + if (rand <= 0) { + message = messages[i].message; + break; + } + } + + return ( + <div className={styles.background}> + <div className={styles.container}> + <div className={styles.blockAnimation}> + <img + className={styles.topBlock} + src={topBlock} + /> + <img + className={styles.middleBlock} + src={middleBlock} + /> + <img + className={styles.bottomBlock} + src={bottomBlock} + /> + </div> + <h1 className={styles.title}> + <FormattedMessage + defaultMessage="Loading Project" + description="Main loading message" + id="gui.loader.headline" + /> + </h1> + <p>{message}</p> + </div> + </div> + ); +}; + +export default LoaderComponent; diff --git a/src/components/loader/middle-block.svg b/src/components/loader/middle-block.svg new file mode 100644 index 0000000000000000000000000000000000000000..6e52d4213a9e8bfff2c8a0289fa324cfcbd60c27 Binary files /dev/null and b/src/components/loader/middle-block.svg differ diff --git a/src/components/loader/top-block.svg b/src/components/loader/top-block.svg new file mode 100644 index 0000000000000000000000000000000000000000..2b52a0d5d09684056643fb04aec7c80967569c95 Binary files /dev/null and b/src/components/loader/top-block.svg differ diff --git a/src/containers/error-boundary.jsx b/src/containers/error-boundary.jsx index 210520bf51bddc91260132c78900cf941e6a5646..d7f558c166f3da16eb00b4a88423add555a994e1 100644 --- a/src/containers/error-boundary.jsx +++ b/src/containers/error-boundary.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import platform from 'platform'; import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; +import CrashMessageComponent from '../components/crash-message/crash-message.jsx'; import log from '../lib/log.js'; import analytics from '../lib/analytics'; @@ -28,21 +29,16 @@ class ErrorBoundary extends React.Component { window.history.back(); } + handleReload () { + window.location.replace(window.location.origin); + } + render () { if (this.state.hasError) { if (platform.name === 'IE') { return <BrowserModalComponent onBack={this.handleBack} />; } - return ( - <div style={{margin: '2rem'}}> - <h1>Oops! Something went wrong.</h1> - <p> - We are so sorry, but it looks like Scratch has crashed. This bug has been - automatically reported to the Scratch Team. Please refresh your page to try - again. - </p> - </div> - ); + return <CrashMessageComponent onReload={this.handleReload} />; } return this.props.children; } diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index de4fe27a284623c4058f07e62da59d32fd378643..320eedcdb6661a076d39bea33892c9e4a0deefa4 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -17,16 +17,29 @@ import vmListenerHOC from '../lib/vm-listener-hoc.jsx'; import GUIComponent from '../components/gui/gui.jsx'; class GUI extends React.Component { + constructor (props) { + super(props); + this.state = { + loading: true + }; + } componentDidMount () { this.audioEngine = new AudioEngine(); this.props.vm.attachAudioEngine(this.audioEngine); - this.props.vm.loadProject(this.props.projectData); - this.props.vm.setCompatibilityMode(true); - this.props.vm.start(); + this.props.vm.loadProject(this.props.projectData).then(() => { + this.setState({loading: false}, () => { + this.props.vm.setCompatibilityMode(true); + this.props.vm.start(); + }); + }); } componentWillReceiveProps (nextProps) { if (this.props.projectData !== nextProps.projectData) { - this.props.vm.loadProject(nextProps.projectData); + this.setState({loading: true}, () => { + this.props.vm.loadProject(nextProps.projectData).then(() => { + this.setState({loading: false}); + }); + }); } } componentWillUnmount () { @@ -35,12 +48,14 @@ class GUI extends React.Component { render () { const { children, + fetchingProject, projectData, // eslint-disable-line no-unused-vars vm, ...componentProps } = this.props; return ( <GUIComponent + loading={fetchingProject || this.state.loading} vm={vm} {...componentProps} > @@ -53,6 +68,7 @@ class GUI extends React.Component { GUI.propTypes = { ...GUIComponent.propTypes, feedbackFormVisible: PropTypes.bool, + fetchingProject: PropTypes.bool, importInfoVisible: PropTypes.bool, previewInfoVisible: PropTypes.bool, projectData: PropTypes.string, diff --git a/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png b/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png deleted file mode 100755 index da373d2cf3ab7c4c617d307eec5a044ae43917ee..0000000000000000000000000000000000000000 Binary files a/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png and /dev/null differ diff --git a/src/lib/default-project/index.js b/src/lib/default-project/index.js index bc1ace99984c8fc8982f59dcba5fdcb3f3bb7386..d0589f6113eac9ceb788f2b0020d95b29bb6efe4 100644 --- a/src/lib/default-project/index.js +++ b/src/lib/default-project/index.js @@ -5,7 +5,6 @@ import projectJson from './project.json'; import popWav from '!buffer-loader!./83a9787d4cb6f3b7632b4ddfebf74367.wav'; import meowWav from '!buffer-loader!./83c36d806dc92327b9e7049a565c6bff.wav'; import backdrop from '!buffer-loader!./739b5e2a2435f6e1ec2993791b423146.png'; -import penLayer from '!buffer-loader!./5c81a336fab8be57adc039a8a2b33ca9.png'; import costume1 from '!raw-loader!./09dc888b0b7df19f70d81588ae73420e.svg'; import costume2 from '!raw-loader!./3696356a03a8d938318876a593572843.svg'; /* eslint-enable import/no-unresolved */ @@ -31,11 +30,6 @@ export default [{ assetType: 'ImageBitmap', dataFormat: 'PNG', data: backdrop -}, { - id: '5c81a336fab8be57adc039a8a2b33ca9', - assetType: 'ImageBitmap', - dataFormat: 'PNG', - data: penLayer }, { id: '09dc888b0b7df19f70d81588ae73420e', assetType: 'ImageVector', diff --git a/src/lib/default-project/project.json b/src/lib/default-project/project.json old mode 100644 new mode 100755 index 165f391e361ecc1b98b1b6f602a0e471858a6098..c1ae335d848b25a005b0fc353766ce6a5be05042 --- a/src/lib/default-project/project.json +++ b/src/lib/default-project/project.json @@ -1,67 +1 @@ -{ - "objName": "Stage", - "sounds": [{ - "soundName": "pop", - "soundID": 1, - "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", - "sampleCount": 258, - "rate": 11025, - "format": "" - }], - "costumes": [{ - "costumeName": "backdrop1", - "baseLayerID": 2, - "baseLayerMD5": "739b5e2a2435f6e1ec2993791b423146.png", - "bitmapResolution": 1, - "rotationCenterX": 240, - "rotationCenterY": 180 - }], - "currentCostumeIndex": 0, - "penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png", - "penLayerID": -1, - "tempoBPM": 60, - "videoAlpha": 0.5, - "variables": [{ - "name": "my variable", - "isPersistent": false, - "value": 0 - }], - "children": [{ - "objName": "Sprite1", - "sounds": [{ - "soundName": "Meow", - "soundID": 0, - "md5": "83c36d806dc92327b9e7049a565c6bff.wav", - "sampleCount": 18688, - "rate": 22050, - "format": "" - }], - "costumes": [{ - "costumeName": "costume1", - "baseLayerID": 0, - "baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg", - "bitmapResolution": 1, - "rotationCenterX": 47, - "rotationCenterY": 55 - }, - { - "costumeName": "costume2", - "baseLayerID": 1, - "baseLayerMD5": "3696356a03a8d938318876a593572843.svg", - "bitmapResolution": 1, - "rotationCenterX": 47, - "rotationCenterY": 55 - }], - "currentCostumeIndex": 0, - "scratchX": 0, - "scratchY": 0, - "scale": 1, - "direction": 90, - "rotationStyle": "normal", - "isDraggable": false, - "indexInLibrary": 1, - "visible": true, - "spriteInfo": { - } - }] -} +{"targets":[{"id":"`jEk@4|i[#Fk?(8x)AV.","name":"Stage","isStage":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"currentCostume":0,"costume":{"name":"backdrop1","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":2,"dataFormat":"png","assetId":"739b5e2a2435f6e1ec2993791b423146"},"costumeCount":1,"visible":true,"rotationStyle":"all around","blocks":{},"variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":{"id":"`jEk@4|i[#Fk?(8x)AV.-my variable","name":"my variable","type":"","isCloud":false,"value":0}},"lists":{},"costumes":[{"name":"backdrop1","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":2,"dataFormat":"png","assetId":"739b5e2a2435f6e1ec2993791b423146"}],"sounds":[{"name":"pop","format":"","rate":11025,"sampleCount":258,"soundID":1,"md5":"83a9787d4cb6f3b7632b4ddfebf74367.wav","data":null,"dataFormat":"wav","assetId":"83a9787d4cb6f3b7632b4ddfebf74367","soundId":"p=i?*Zt*I]@]x_*V`mut"}]},{"id":"9xJ@2eKXvx:/*Q^3Rib#","name":"Sprite1","isStage":false,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"currentCostume":0,"costume":{"name":"costume1","bitmapResolution":1,"rotationCenterX":47,"rotationCenterY":55,"skinId":0,"dataFormat":"svg","assetId":"09dc888b0b7df19f70d81588ae73420e"},"costumeCount":2,"visible":true,"rotationStyle":"all around","blocks":{},"variables":{},"lists":{},"costumes":[{"name":"costume1","bitmapResolution":1,"rotationCenterX":47,"rotationCenterY":55,"skinId":0,"dataFormat":"svg","assetId":"09dc888b0b7df19f70d81588ae73420e"},{"name":"costume2","bitmapResolution":1,"rotationCenterX":47,"rotationCenterY":55,"skinId":1,"dataFormat":"svg","assetId":"3696356a03a8d938318876a593572843"}],"sounds":[{"name":"Meow","format":"","rate":22050,"sampleCount":18688,"soundID":0,"md5":"83c36d806dc92327b9e7049a565c6bff.wav","data":null,"dataFormat":"wav","assetId":"83c36d806dc92327b9e7049a565c6bff","soundId":"]z6@jLeJ2W%gr/eA1HB+"}]}],"meta":{"semver":"3.0.0","vm":"0.1.0","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36"}} \ No newline at end of file diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index 854928c7eaa2720dff7150c586faf9e179d90116..2c20b9fed50736b490643d02ca40c06f11a051a0 100644 --- a/src/lib/project-loader-hoc.jsx +++ b/src/lib/project-loader-hoc.jsx @@ -17,21 +17,25 @@ const ProjectLoaderHOC = function (WrappedComponent) { this.updateProject = this.updateProject.bind(this); this.state = { projectId: null, - projectData: null + projectData: null, + fetchingProject: false }; } componentDidMount () { window.addEventListener('hashchange', this.updateProject); this.updateProject(); } - componentDidUpdate (prevProps, prevState) { - if (this.state.projectId !== prevState.projectId) { - storage - .load(storage.AssetType.Project, this.state.projectId, storage.DataFormat.JSON) - .then(projectAsset => projectAsset && this.setState({ - projectData: projectAsset.data.toString() - })) - .catch(err => log.error(err)); + componentWillUpdate (nextProps, nextState) { + if (this.state.projectId !== nextState.projectId) { + this.setState({fetchingProject: true}, () => { + storage + .load(storage.AssetType.Project, this.state.projectId, storage.DataFormat.JSON) + .then(projectAsset => projectAsset && this.setState({ + projectData: projectAsset.data.toString(), + fetchingProject: false + })) + .catch(err => log.error(err)); + }); } } componentWillUnmount () { @@ -60,6 +64,7 @@ const ProjectLoaderHOC = function (WrappedComponent) { if (!this.state.projectData) return null; return ( <WrappedComponent + fetchingProject={this.state.fetchingProject} projectData={this.state.projectData} {...this.props} />