diff --git a/package.json b/package.json index 439bf307995147592165e46d4004978f0ab17314..94658acec5d21837858cce186d8a21fc759ff7cc 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "scratch-render": "0.1.0-prerelease.20180502115145", "scratch-svg-renderer": "0.1.0-prerelease.20180423193917", "scratch-storage": "0.4.1", - "scratch-vm": "0.1.0-prerelease.1525459625", + "scratch-vm": "0.1.0-prerelease.1525460669", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.20.0", diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index d3b2f9d4480de7260e6a15d2650077e1486dd0bd..21cef41e9a6c66b2072b2625d6ad3f1a828f7154 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -34,7 +34,7 @@ const messages = defineMessages({ addSpriteFromFile: { id: 'gui.spriteSelector.addSpriteFromFile', description: 'Button to add a sprite in the target pane from file', - defaultMessage: 'Coming Soon' + defaultMessage: 'Upload' } }); @@ -51,12 +51,15 @@ const SpriteSelectorComponent = function (props) { onChangeSpriteY, onDeleteSprite, onDuplicateSprite, + onFileUploadClick, onNewSpriteClick, - onSurpriseSpriteClick, onPaintSpriteClick, onSelectSprite, + onSpriteUpload, + onSurpriseSpriteClick, raised, selectedId, + spriteFileInput, sprites, ...componentProps } = props; @@ -121,7 +124,11 @@ const SpriteSelectorComponent = function (props) { moreButtons={[ { title: intl.formatMessage(messages.addSpriteFromFile), - img: fileUploadIcon + img: fileUploadIcon, + onClick: onFileUploadClick, + fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2', // TODO add sprite 3 + fileChange: onSpriteUpload, + fileInput: spriteFileInput }, { title: intl.formatMessage(messages.addSpriteFromSurprise), img: surpriseIcon, @@ -154,12 +161,15 @@ SpriteSelectorComponent.propTypes = { onChangeSpriteY: PropTypes.func, onDeleteSprite: PropTypes.func, onDuplicateSprite: PropTypes.func, + onFileUploadClick: PropTypes.func, onNewSpriteClick: PropTypes.func, onPaintSpriteClick: PropTypes.func, onSelectSprite: PropTypes.func, + onSpriteUpload: PropTypes.func, onSurpriseSpriteClick: PropTypes.func, raised: PropTypes.bool, selectedId: PropTypes.string, + spriteFileInput: PropTypes.func, sprites: PropTypes.shape({ id: PropTypes.shape({ costume: PropTypes.shape({ diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index 4be489f6948e718c7fe8c2cecb90c44db388726a..fb08f3b413c8da8105a40309571c4c5a95cb383b 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -17,6 +17,7 @@ import styles from './target-pane.css'; */ const TargetPane = ({ editingTarget, + fileInputRef, hoveredTarget, spriteLibraryVisible, onChangeSpriteDirection, @@ -27,11 +28,13 @@ const TargetPane = ({ onChangeSpriteY, onDeleteSprite, onDuplicateSprite, + onFileUploadClick, onNewSpriteClick, - onSurpriseSpriteClick, onPaintSpriteClick, onRequestCloseSpriteLibrary, onSelectSprite, + onSpriteUpload, + onSurpriseSpriteClick, raiseSprites, stage, sprites, @@ -48,6 +51,7 @@ const TargetPane = ({ hoveredTarget={hoveredTarget} raised={raiseSprites} selectedId={editingTarget} + spriteFileInput={fileInputRef} sprites={sprites} onChangeSpriteDirection={onChangeSpriteDirection} onChangeSpriteName={onChangeSpriteName} @@ -57,9 +61,11 @@ const TargetPane = ({ onChangeSpriteY={onChangeSpriteY} onDeleteSprite={onDeleteSprite} onDuplicateSprite={onDuplicateSprite} + onFileUploadClick={onFileUploadClick} onNewSpriteClick={onNewSpriteClick} onPaintSpriteClick={onPaintSpriteClick} onSelectSprite={onSelectSprite} + onSpriteUpload={onSpriteUpload} onSurpriseSpriteClick={onSurpriseSpriteClick} /> <div className={styles.stageSelectorWrapper}> @@ -108,6 +114,7 @@ const spriteShape = PropTypes.shape({ TargetPane.propTypes = { editingTarget: PropTypes.string, extensionLibraryVisible: PropTypes.bool, + fileInputRef: PropTypes.func, hoveredTarget: PropTypes.shape({ hoveredSprite: PropTypes.string, receivedBlocks: PropTypes.bool @@ -120,11 +127,13 @@ TargetPane.propTypes = { onChangeSpriteY: PropTypes.func, onDeleteSprite: PropTypes.func, onDuplicateSprite: PropTypes.func, + onFileUploadClick: PropTypes.func, onNewSpriteClick: PropTypes.func, onPaintSpriteClick: PropTypes.func, onRequestCloseExtensionLibrary: PropTypes.func, onRequestCloseSpriteLibrary: PropTypes.func, onSelectSprite: PropTypes.func, + onSpriteUpload: PropTypes.func, onSurpriseSpriteClick: PropTypes.func, raiseSprites: PropTypes.bool, spriteLibraryVisible: PropTypes.bool, diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx index 072e1714a81ca1683acb86e9d280bef47c322156..15d75970c1b9123373d37e7c93d3a4aa20fe4c4c 100644 --- a/src/containers/sprite-library.jsx +++ b/src/containers/sprite-library.jsx @@ -39,7 +39,7 @@ class SpriteLibrary extends React.PureComponent { clearInterval(this.intervalId); } handleItemSelect (item) { - this.props.vm.addSprite2(JSON.stringify(item.json)); + this.props.vm.addSprite(JSON.stringify(item.json)); analytics.event({ category: 'library', action: 'Select Sprite', diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 97d6dffaa31e4c89e9f09a02d0e7acda5bb8fe76..1178ed64ec9d7fc10eb6d07afe1fcba16a535ffb 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -13,6 +13,7 @@ import {setReceivedBlocks} from '../reducers/hovered-target'; import TargetPaneComponent from '../components/target-pane/target-pane.jsx'; import spriteLibraryContent from '../lib/libraries/sprites.json'; +import {handleFileUpload, spriteUpload} from '../lib/file-uploader.js'; class TargetPane extends React.Component { constructor (props) { @@ -27,9 +28,13 @@ class TargetPane extends React.Component { 'handleChangeSpriteY', 'handleDeleteSprite', 'handleDuplicateSprite', + 'handleNewSprite', 'handleSelectSprite', 'handleSurpriseSpriteClick', - 'handlePaintSpriteClick' + 'handlePaintSpriteClick', + 'handleFileUploadClick', + 'handleSpriteUpload', + 'setFileInput' ]); } componentDidMount () { @@ -67,19 +72,34 @@ class TargetPane extends React.Component { } handleSurpriseSpriteClick () { const item = spriteLibraryContent[Math.floor(Math.random() * spriteLibraryContent.length)]; - this.props.vm.addSprite2(JSON.stringify(item.json)); + this.props.vm.addSprite(JSON.stringify(item.json)); } handlePaintSpriteClick () { // @todo this is brittle, will need to be refactored for localized libraries const emptyItem = spriteLibraryContent.find(item => item.name === 'Empty'); if (emptyItem) { - this.props.vm.addSprite2(JSON.stringify(emptyItem.json)).then(() => { + this.props.vm.addSprite(JSON.stringify(emptyItem.json)).then(() => { setTimeout(() => { // Wait for targets update to propagate before tab switching this.props.onActivateTab(COSTUMES_TAB_INDEX); }); }); } } + handleNewSprite (spriteJSONString) { + this.props.vm.addSprite(spriteJSONString); + } + handleFileUploadClick () { + this.fileInput.click(); + } + handleSpriteUpload (e) { + const storage = this.props.vm.runtime.storage; + handleFileUpload(e.target, (buffer, fileType, fileName) => { + spriteUpload(buffer, fileType, fileName, storage, this.handleNewSprite); + }); + } + setFileInput (input) { + this.fileInput = input; + } handleBlockDragEnd (blocks) { if (this.props.hoveredTarget.sprite && this.props.hoveredTarget.sprite !== this.props.editingTarget) { this.props.vm.shareBlocksToTarget(blocks, this.props.hoveredTarget.sprite); @@ -95,6 +115,7 @@ class TargetPane extends React.Component { return ( <TargetPaneComponent {...componentProps} + fileInputRef={this.setFileInput} onChangeSpriteDirection={this.handleChangeSpriteDirection} onChangeSpriteName={this.handleChangeSpriteName} onChangeSpriteSize={this.handleChangeSpriteSize} @@ -103,8 +124,10 @@ class TargetPane extends React.Component { onChangeSpriteY={this.handleChangeSpriteY} onDeleteSprite={this.handleDeleteSprite} onDuplicateSprite={this.handleDuplicateSprite} + onFileUploadClick={this.handleFileUploadClick} onPaintSpriteClick={this.handlePaintSpriteClick} onSelectSprite={this.handleSelectSprite} + onSpriteUpload={this.handleSpriteUpload} onSurpriseSpriteClick={this.handleSurpriseSpriteClick} /> ); diff --git a/src/lib/file-uploader.js b/src/lib/file-uploader.js index ca4ce8773c113b92ef9fcfb460daeda609242482..d32f5cc1cdf4d055ba9d5078d7bc9589e2d8002b 100644 --- a/src/lib/file-uploader.js +++ b/src/lib/file-uploader.js @@ -47,6 +47,7 @@ const handleFileUpload = function (fileInput, onload) { * @property {string} dataFormat The data format of this asset, typically * the extension to be used for that particular asset, e.g. 'svg' for vector images * @property {string} md5 The md5 hash of the asset data, followed by '.'' and dataFormat + * @property {string} The md5 hash of the asset data // TODO remove duplication.... */ /** @@ -71,7 +72,8 @@ const cacheAsset = function (storage, fileName, assetType, dataFormat, data) { return { name: fileName, dataFormat: dataFormat, - md5: `${md5}.${dataFormat}` + md5: `${md5}.${dataFormat}`, + assetId: md5 }; }; @@ -106,6 +108,7 @@ const costumeUpload = function (fileData, fileType, costumeName, storage, handle break; } default: + log.warn(`Encountered unexpected file type: ${fileType}`); return; } @@ -158,6 +161,7 @@ const soundUpload = function (fileData, fileType, soundName, storage, handleSoun break; } default: + log.warn(`Encountered unexpected file type: ${fileType}`); return; } @@ -171,8 +175,49 @@ const soundUpload = function (fileData, fileType, soundName, storage, handleSoun handleSound(vmSound); }; +const spriteUpload = function (fileData, fileType, spriteName, storage, handleSprite) { + switch (fileType) { + case '': + case 'application/zip': { // We think this is a .sprite2 or .sprite3 file + handleSprite(new Uint8Array(fileData)); + return; + } + case 'image/svg+xml': + case 'image/png': + case 'image/jpeg': { + // Make a sprite from an image by making it a costume first + costumeUpload(fileData, fileType, `${spriteName}-costume1`, storage, (vmCostume => { + const newSprite = { + name: spriteName, + isStage: false, + x: 0, + y: 0, + visible: true, + size: 100, + rotationStyle: 'all around', + direction: 90, + draggable: true, + currentCostume: 0, + blocks: {}, + variables: {}, + costumes: [vmCostume], + sounds: [] // TODO are all of these necessary? + }; + // TODO probably just want sprite upload to handle this object directly + handleSprite(JSON.stringify(newSprite)); + })); + return; + } + default: { + log.warn(`Encountered unexpected file type: ${fileType}`); + return; + } + } +}; + export { handleFileUpload, costumeUpload, - soundUpload + soundUpload, + spriteUpload };