diff --git a/package.json b/package.json index 24b2fabe5366c9ec13e322f6a9fe9ee296f62bf1..a3ff0b80b12f0ff763551c8bd08a572f0b2f286a 100644 --- a/package.json +++ b/package.json @@ -95,17 +95,13 @@ "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1530135682", - "scratch-l10n": "3.0.20180611175036", - "scratch-paint": "0.2.0-prerelease.20180629115559", - "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1530135682", - "scratch-l10n": "3.0.20180627134459", - "scratch-paint": "0.2.0-prerelease.20180629115559", + "scratch-blocks": "0.1.0-prerelease.1531144787", + "scratch-l10n": "3.0.20180703181510", + "scratch-paint": "0.2.0-prerelease.20180709132225", "scratch-render": "0.1.0-prerelease.20180618173030", "scratch-storage": "0.5.1", "scratch-svg-renderer": "0.2.0-prerelease.20180618172917", - "scratch-vm": "0.1.0-prerelease.1529940524", + "scratch-vm": "0.1.0-prerelease.1530902855", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.21.0", diff --git a/src/components/forms/label.css b/src/components/forms/label.css index 8ec6283bf7b61e91ad231780e7a4e9b596b17379..759a731d37efca6effb443534f24ed17e30bec5c 100644 --- a/src/components/forms/label.css +++ b/src/components/forms/label.css @@ -12,6 +12,8 @@ margin-right: .5rem; user-select: none; cursor: default; + + white-space: nowrap; } .input-label { diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index af7500fb0a3d9e0fd57c3bda81a3fa3fe5d97a95..b53f17b55ebd42e6cc7cad2dae2d6903dbcb44cb 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -79,6 +79,7 @@ align-items: center; user-select: none; + white-space: nowrap; } /* Use z-indices to force left-on-top for tabs */ diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css index 34a8f5a21e3aea6ef4817aa6a2158232ca59d18d..78e3ecb27c6b3d2583528ed1dc6b1df6f36dca6e 100644 --- a/src/components/prompt/prompt.css +++ b/src/components/prompt/prompt.css @@ -15,7 +15,7 @@ margin: 0 0 0.75rem; } -.input { +.variable-name-text-input { margin-bottom: 1.5rem; width: 100%; border: 1px solid $ui-black-transparent; @@ -26,6 +26,20 @@ font-size: .875rem; } +.info-message { + font-weight: normal; + font-size: .875rem; + margin-bottom: 1.5rem; + text-align: center; +} + +.options-row { + display: flex; + font-weight: normal; + justify-content: space-between; + margin-bottom: 1.5rem; +} + .button-row { font-weight: bolder; text-align: right; @@ -58,10 +72,6 @@ margin: 0 0 1rem; } -.hide-more-options { - display: none; -} - .more-options-accordion { width: 60%; margin: 0 auto; diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx index 4feffeb0c8205784938ba1e586c0027bc9de6a05..3e5ec7950fc1be25a4e5cc89297a9828237fd649 100644 --- a/src/components/prompt/prompt.jsx +++ b/src/components/prompt/prompt.jsx @@ -11,10 +11,26 @@ import styles from './prompt.css'; import dropdownIcon from './icon--dropdown-caret.svg'; const messages = defineMessages({ + forAllSpritesMessage: { + defaultMessage: 'For all sprites', + description: 'Option message when creating a variable for making it available to all sprites', + id: 'gui.gui.variableScopeOptionAllSprites' + }, + forThisSpriteMessage: { + defaultMessage: 'For this sprite only', + description: 'Option message when creating a varaible for making it only available to the current sprite', + id: 'gui.gui.variableScopeOptionSpriteOnly' + }, moreOptionsMessage: { defaultMessage: 'More Options', description: 'Dropdown message for variable/list options', id: 'gui.gui.variablePrompt' + }, + availableToAllSpritesMessage: { + defaultMessage: 'This variable will be available to all sprites.', + description: 'A message that displays in a variable modal when the stage is selected indicating ' + + 'that the variable being created will available to all sprites.', + id: 'gui.gui.variablePromptAllSpritesMessage' } }); @@ -31,29 +47,64 @@ const PromptComponent = props => ( <Box> <input autoFocus - className={styles.input} + className={styles.variableNameTextInput} placeholder={props.placeholder} onChange={props.onChange} onKeyPress={props.onKeyPress} /> </Box> - <Box className={props.showMoreOptions ? styles.moreOptions : styles.hideMoreOptions}> - <ComingSoonTooltip - className={styles.moreOptionsAccordion} - place="right" - tooltipId="variable-options-accordion" - > - <div className={styles.moreOptionsText}> - <FormattedMessage - {...messages.moreOptionsMessage} - /> - <img - className={styles.moreOptionsIcon} - src={dropdownIcon} - /> - </div> - </ComingSoonTooltip> - </Box> + {props.showMoreOptions ? + <div> + {props.isStage ? + <div className={styles.infoMessage}> + <FormattedMessage + {...messages.availableToAllSpritesMessage} + /> + </div> : + <Box className={styles.optionsRow}> + <label> + <input + defaultChecked + name="variableScopeOption" + type="radio" + value="global" + onChange={props.onOptionSelection} + /> + <FormattedMessage + {...messages.forAllSpritesMessage} + /> + </label> + <label> + <input + name="variableScopeOption" + type="radio" + value="local" + onChange={props.onOptionSelection} + /> + <FormattedMessage + {...messages.forThisSpriteMessage} + /> + </label> + </Box>} + <Box className={styles.moreOptions}> + <ComingSoonTooltip + className={styles.moreOptionsAccordion} + place="right" + tooltipId="variable-options-accordion" + > + <div className={styles.moreOptionsText}> + <FormattedMessage + {...messages.moreOptionsMessage} + /> + <img + className={styles.moreOptionsIcon} + src={dropdownIcon} + /> + </div> + </ComingSoonTooltip> + </Box> + </div> : null} + <Box className={styles.buttonRow}> <button className={styles.cancelButton} @@ -81,11 +132,13 @@ const PromptComponent = props => ( ); PromptComponent.propTypes = { + isStage: PropTypes.bool.isRequired, label: PropTypes.string.isRequired, onCancel: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onKeyPress: PropTypes.func.isRequired, onOk: PropTypes.func.isRequired, + onOptionSelection: PropTypes.func.isRequired, placeholder: PropTypes.string, showMoreOptions: PropTypes.bool.isRequired, title: PropTypes.string.isRequired diff --git a/src/components/sprite-info/sprite-info.css b/src/components/sprite-info/sprite-info.css index 53758e65cc344038befbdae8066d41e14f2379c7..c835843994ce4ffb477a742fdd48c6461c288992 100644 --- a/src/components/sprite-info/sprite-info.css +++ b/src/components/sprite-info/sprite-info.css @@ -44,6 +44,10 @@ } /* @todo: refactor radio divs to input */ +.radio-wrapper { + white-space: nowrap; /* make sure visibilty buttons don't wrap */ +} + .radio { filter: saturate(0); cursor: pointer; diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx index 09d5c96c7a0da7253f18b5652d22a4ae6d7c5272..37e18735a8c6b2bddbba1443f356b820f50ca321 100644 --- a/src/components/sprite-info/sprite-info.jsx +++ b/src/components/sprite-info/sprite-info.jsx @@ -176,7 +176,7 @@ class SpriteInfo extends React.Component { /> : null } - <div> + <div className={styles.radioWrapper}> <div className={classNames( styles.radio, diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx index e8ab7f8d0cc583f25801617f53399ae0c78b10a1..a29912b79385183e8c49e0ac819ba5288ec3e761 100644 --- a/src/components/sprite-selector/sprite-list.jsx +++ b/src/components/sprite-selector/sprite-list.jsx @@ -71,6 +71,7 @@ const SpriteList = function (props) { [styles.raised]: isRaised, [styles.receivedBlocks]: receivedBlocks })} + dragPayload={sprite} dragType={DragConstants.SPRITE} id={sprite.id} index={index} diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx index 35fc11bf1655f2b40a0453ce55f12b04d9d1ad7f..aedf2f35da1c84187386aae3e5132d7fbfad9372 100644 --- a/src/containers/backpack.jsx +++ b/src/containers/backpack.jsx @@ -7,12 +7,14 @@ import { saveBackpackObject, deleteBackpackObject, soundPayload, - costumePayload + costumePayload, + spritePayload } from '../lib/backpack-api'; import DragConstants from '../lib/drag-constants'; import {connect} from 'react-redux'; import storage from '../lib/storage'; +import VM from 'scratch-vm'; class Backpack extends React.Component { constructor (props) { @@ -45,7 +47,7 @@ class Backpack extends React.Component { } } componentWillReceiveProps (newProps) { - const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND]; + const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE]; // If `dragging` becomes true, record the drop area rectangle if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) { this.dropAreaRect = this.ref && this.ref.getBoundingClientRect(); @@ -83,10 +85,13 @@ class Backpack extends React.Component { case DragConstants.SOUND: payloader = soundPayload; break; + case DragConstants.SPRITE: + payloader = spritePayload; + break; } if (!payloader) return; - payloader(dragInfo.payload) + payloader(dragInfo.payload, this.props.vm) .then(payload => saveBackpackObject({ host: this.props.host, token: this.props.token, @@ -152,7 +157,8 @@ Backpack.propTypes = { }), host: PropTypes.string, token: PropTypes.string, - username: PropTypes.string + username: PropTypes.string, + vm: PropTypes.instanceOf(VM) }; const getTokenAndUsername = state => { diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index 985211b06a6973bde35948764792eeb7816ea9a0..ca327d2cefc8f538fbef34ca05758a86eb3aef06 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -328,9 +328,13 @@ class Blocks extends React.Component { handlePromptStart (message, defaultValue, callback, optTitle, optVarType) { const p = {prompt: {callback, message, defaultValue}}; p.prompt.title = optTitle ? optTitle : - this.ScratchBlocks.VARIABLE_MODAL_TITLE; + this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; + p.prompt.varType = typeof optVarType === 'string' ? + optVarType : this.ScratchBlocks.SCALAR_VARIABLE_TYPE; p.prompt.showMoreOptions = - optVarType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE; + optVarType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE && + p.prompt.title !== this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE && + p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE; this.setState(p); } handleConnectionModalStart (extensionId) { @@ -345,8 +349,10 @@ class Blocks extends React.Component { handleStatusButtonUpdate (extensionId, status) { this.ScratchBlocks.updateStatusButton(this.workspace, extensionId, status); } - handlePromptCallback (data) { - this.state.prompt.callback(data); + handlePromptCallback (input, optionSelection) { + this.state.prompt.callback(input, optionSelection, + (optionSelection === 'local') ? [] : + this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType)); this.handlePromptClose(); } handlePromptClose () { @@ -385,6 +391,7 @@ class Blocks extends React.Component { /> {this.state.prompt ? ( <Prompt + isStage={vm.runtime.getEditingTarget().isStage} label={this.state.prompt.message} placeholder={this.state.prompt.defaultValue} showMoreOptions={this.state.prompt.showMoreOptions} diff --git a/src/containers/prompt.jsx b/src/containers/prompt.jsx index a798b02ed780ea8a3dbe5a31bad4e17aa3b66226..72c8ad8a376ff22ecef130e875053ed79c2a435b 100644 --- a/src/containers/prompt.jsx +++ b/src/containers/prompt.jsx @@ -8,19 +8,21 @@ class Prompt extends React.Component { super(props); bindAll(this, [ 'handleOk', + 'handleOptionSelection', 'handleCancel', 'handleChange', 'handleKeyPress' ]); this.state = { - inputValue: '' + inputValue: '', + optionSelection: null }; } handleKeyPress (event) { if (event.key === 'Enter') this.handleOk(); } handleOk () { - this.props.onOk(this.state.inputValue); + this.props.onOk(this.state.inputValue, this.state.optionSelection); } handleCancel () { this.props.onCancel(); @@ -28,9 +30,13 @@ class Prompt extends React.Component { handleChange (e) { this.setState({inputValue: e.target.value}); } + handleOptionSelection (e) { + this.setState({optionSelection: e.target.value}); + } render () { return ( <PromptComponent + isStage={this.props.isStage} label={this.props.label} placeholder={this.props.placeholder} showMoreOptions={this.props.showMoreOptions} @@ -39,12 +45,14 @@ class Prompt extends React.Component { onChange={this.handleChange} onKeyPress={this.handleKeyPress} onOk={this.handleOk} + onOptionSelection={this.handleOptionSelection} /> ); } } Prompt.propTypes = { + isStage: PropTypes.bool.isRequired, label: PropTypes.string.isRequired, onCancel: PropTypes.func.isRequired, onOk: PropTypes.func.isRequired, diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 1d637330acce4a4410f1f6ecd840089c86febfc6..5f9fef8db723287d70b68669a858050c09edf15a 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -135,6 +135,12 @@ class TargetPane extends React.Component { if (dragInfo.dragType === DragConstants.SPRITE) { // Add one to both new and target index because we are not counting/moving the stage this.props.vm.reorderTarget(dragInfo.index + 1, dragInfo.newIndex + 1); + } else if (dragInfo.dragType === DragConstants.BACKPACK_SPRITE) { + // TODO storage does not have a way of loading zips right now, and may never need it. + // So for now just grab the zip manually. + fetch(dragInfo.payload.bodyUrl) + .then(response => response.arrayBuffer()) + .then(sprite3Zip => this.props.vm.addSprite(sprite3Zip)); } else if (targetId) { // Something is being dragged over one of the sprite tiles or the backdrop. // Dropping assets like sounds and costumes duplicate the asset on the diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js index 9a2b19e95979a57329cbcd87dac60b895fdb18bf..f0c51f14040ced7823c226521dd792b4cf639f65 100644 --- a/src/lib/backpack-api.js +++ b/src/lib/backpack-api.js @@ -1,6 +1,7 @@ import xhr from 'xhr'; import costumePayload from './backpack/costume-payload'; import soundPayload from './backpack/sound-payload'; +import spritePayload from './backpack/sprite-payload'; const getBackpackContents = ({ host, @@ -19,9 +20,13 @@ const getBackpackContents = ({ return reject(); } // Add a new property for the full thumbnail url, which includes the host. + // Also include a full body url for loading sprite zips // TODO retreiving the images through storage would allow us to remove this. return resolve(response.body.map(item => ( - Object.assign({}, item, {thumbnailUrl: `${host}/${item.thumbnail}`}) + Object.assign({}, item, { + thumbnailUrl: `${host}/${item.thumbnail}`, + bodyUrl: `${host}/${item.body}` + }) ))); }); }); @@ -72,5 +77,6 @@ export { saveBackpackObject, deleteBackpackObject, costumePayload, - soundPayload + soundPayload, + spritePayload }; diff --git a/src/lib/backpack/sprite-payload.js b/src/lib/backpack/sprite-payload.js new file mode 100644 index 0000000000000000000000000000000000000000..c9dea7f22ab4cf9933463540903dada2c4a6206c --- /dev/null +++ b/src/lib/backpack/sprite-payload.js @@ -0,0 +1,24 @@ +import jpegThumbnail from './jpeg-thumbnail'; +import storage from '../storage'; + +const spritePayload = (sprite, vm) => vm.exportSprite( + sprite.id, + 'base64' +).then(zippedSprite => { + const payload = { + type: 'sprite', + name: sprite.name, + mime: 'application/zip', + body: zippedSprite, + // Filled in below + thumbnail: '' + }; + + const costumeDataUrl = storage.get(sprite.costume.assetId).encodeDataURI(); + return jpegThumbnail(costumeDataUrl).then(thumbnail => { + payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', ''); + return payload; + }); +}); + +export default spritePayload; diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 2fea2f0cb33964845f12a628c9d9b59fa4a06beb..1c2a0ae1936d2ef2d373ba87437bc696e80d3467 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -553,7 +553,7 @@ const operators = function () { </value> <value name="OPERAND2"> <shadow type="text"> - <field name="TEXT">100</field> + <field name="TEXT">50</field> </shadow> </value> </block> @@ -565,7 +565,7 @@ const operators = function () { </value> <value name="OPERAND2"> <shadow type="text"> - <field name="TEXT">100</field> + <field name="TEXT">50</field> </shadow> </value> </block> @@ -577,7 +577,7 @@ const operators = function () { </value> <value name="OPERAND2"> <shadow type="text"> - <field name="TEXT">100</field> + <field name="TEXT">50</field> </shadow> </value> </block>