diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css new file mode 100644 index 00000000000..c222216b1a2 --- /dev/null +++ b/src/components/action-menu/action-menu.css @@ -0,0 +1,174 @@ +@import "../../css/colors.css"; + +$main-button-size: 2.75rem; +$more-button-size: 2.25rem; + +.menu-container { + display: flex; + flex-direction: column-reverse; + transition: 0.2s; + position: relative; +} + +.button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background: $motion-primary; + outline: none; + border: none; + transition: background-color 0.2s; +} + +.button:hover { + background: $pen-primary; +} + +.button.coming-soon:hover { + background: $data-primary; +} + +.main-button { + border-radius: 100%; + width: $main-button-size; + height: $main-button-size; + z-index: 20; /* TODO reorder layout to prevent z-index need */ +} + +.main-icon { + width: calc($main-button-size - 1rem); + height: calc($main-button-size - 1rem); +} + +.more-buttons-outer { + /* + Need to use two divs to set different overflow x/y + which is needed to get animation to look right while + allowing the tooltips to be visible. + */ + overflow-y: hidden; + + background: $motion-tertiary; + border-top-left-radius: $more-button-size; + border-top-right-radius: $more-button-size; + width: $more-button-size; + margin-left: calc(($main-button-size - $more-button-size) / 2); + + position: absolute; + bottom: calc($main-button-size); + + margin-bottom: calc($main-button-size / -2); + padding-bottom: calc($main-button-size / 2); +} + +.more-buttons { + max-height: 0; + transition: max-height 1s; + overflow-x: visible; + display: flex; + flex-direction: column; + z-index: 10; /* @todo justify */ +} + +.expanded .more-buttons { + max-height: 1000px; /* Arbitrary, needs to be a value in order for animation to run */ +} + +.force-hidden .more-buttons { + display: none; /* This property does not animate */ +} + +.more-buttons:first-child { /* Round off top button */ + border-top-right-radius: $more-button-size; + border-top-left-radius: $more-button-size; +} + +.more-button { + width: $more-button-size; + height: $more-button-size; + background: $motion-tertiary; +} + +.more-icon { + width: calc($more-button-size - 1rem); + height: calc($more-button-size - 1rem); +} + +.coming-soon .more-icon { + opacity: 0.5; +} + +/* + @todo needs to be refactored with coming soon tooltip overrides. + The "!important"s are for the same reason as with coming soon, the library + is not very easy to style. +*/ +.tooltip { + background-color: $pen-primary !important; + opacity: 1 !important; + border: 1px solid hsla(0, 0%, 0%, .1) !important; + box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; +} + +.tooltip:after { + background-color: $pen-primary; +} + +.coming-soon-tooltip { + background-color: $data-primary !important; +} + +.coming-soon-tooltip:after { + background-color: $data-primary !important; +} + +.tooltip { + border: 1px solid hsla(0, 0%, 0%, .1) !important; + border-radius: .25rem !important; + box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; + z-index: 100 !important; +} + +$arrow-size: 0.5rem; +$arrow-inset: -0.25rem; +$arrow-rounding: 0.125rem; + +.tooltip:after { + content: ""; + border-top: 1px solid hsla(0, 0%, 0%, .1) !important; + border-left: 0 !important; + border-bottom: 0 !important; + border-right: 1px solid hsla(0, 0%, 0%, .1) !important; + border-radius: $arrow-rounding; + height: $arrow-size !important; + width: $arrow-size !important; +} + +.tooltip:global(.place-left):after { + margin-top: $arrow-inset !important; + right: $arrow-inset !important; + transform: rotate(45deg) !important; +} + +.tooltip:global(.place-right):after { + margin-top: $arrow-inset !important; + left: $arrow-inset !important; + transform: rotate(-135deg) !important; +} + +.tooltip:global(.place-top):after { + margin-right: $arrow-inset !important; + bottom: $arrow-inset !important; + transform: rotate(135deg) !important; +} + +.tooltip:global(.place-bottom):after { + margin-left: $arrow-inset !important; + top: $arrow-inset !important; + transform: rotate(-45deg) !important; +} diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx new file mode 100644 index 00000000000..d67c95d8580 --- /dev/null +++ b/src/components/action-menu/action-menu.jsx @@ -0,0 +1,146 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import bindAll from 'lodash.bindall'; +import ReactTooltip from 'react-tooltip'; + +import styles from './action-menu.css'; + +const CLOSE_DELAY = 300; // ms + +class ActionMenu extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClosePopover', + 'handleToggleOpenState', + 'clickDelayer' + ]); + this.state = { + isOpen: false, + forceHide: false + }; + } + handleClosePopover () { + this.closeTimeoutId = setTimeout(() => { + this.setState({isOpen: false}); + this.closeTimeoutId = null; + }, CLOSE_DELAY); + } + handleToggleOpenState () { + // Mouse enter back in after timeout was started prevents it from closing. + if (this.closeTimeoutId) { + clearTimeout(this.closeTimeoutId); + this.closeTimeoutId = null; + } else if (!this.state.isOpen) { + this.setState({ + isOpen: true, + forceHide: false + }); + } + } + clickDelayer (fn) { + // Return a wrapped action that manages the menu closing. + // @todo we may be able to use react-transition for this in the future + // for now all this work is to ensure the menu closes BEFORE the + // (possibly slow) action is started. + return event => { + this.setState({forceHide: true, isOpen: false}, () => { + if (fn) fn(event); + setTimeout(() => this.setState({forceHide: false})); + }); + }; + } + render () { + const { + className, + img: mainImg, + title: mainTitle, + moreButtons, + onClick + } = this.props; + + const mainTooltipId = `tooltip-${Math.random()}`; + + return ( +
+ + +
+
+ {(moreButtons || []).map(({img, title, onClick: handleClick}) => { + const isComingSoon = !handleClick; + const tooltipId = `tooltip-${Math.random()}`; + return ( +
+ + +
+ ); + })} +
+
+
+ ); + } +} + +ActionMenu.propTypes = { + className: PropTypes.string, + img: PropTypes.string, + moreButtons: PropTypes.arrayOf(PropTypes.shape({ + img: PropTypes.string, + title: PropTypes.node.isRequired, + onClick: PropTypes.func // Optional, "coming soon" if no callback provided + })), + onClick: PropTypes.func.isRequired, + title: PropTypes.node.isRequired +}; + +export default ActionMenu; diff --git a/src/components/stage-selector/icon--backdrop.svg b/src/components/action-menu/icon--backdrop.svg similarity index 100% rename from src/components/stage-selector/icon--backdrop.svg rename to src/components/action-menu/icon--backdrop.svg diff --git a/src/components/action-menu/icon--camera.svg b/src/components/action-menu/icon--camera.svg new file mode 100644 index 00000000000..e8c442d8f5d --- /dev/null +++ b/src/components/action-menu/icon--camera.svg @@ -0,0 +1,12 @@ + + + + camera + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/action-menu/icon--file-upload.svg b/src/components/action-menu/icon--file-upload.svg new file mode 100644 index 00000000000..57337d959c1 --- /dev/null +++ b/src/components/action-menu/icon--file-upload.svg @@ -0,0 +1,12 @@ + + + + file-upload + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/action-menu/icon--paint.svg b/src/components/action-menu/icon--paint.svg new file mode 100644 index 00000000000..f79d562c350 --- /dev/null +++ b/src/components/action-menu/icon--paint.svg @@ -0,0 +1,12 @@ + + + + paint + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/sprite-selector/icon--sprite.svg b/src/components/action-menu/icon--sprite.svg similarity index 100% rename from src/components/sprite-selector/icon--sprite.svg rename to src/components/action-menu/icon--sprite.svg diff --git a/src/components/action-menu/icon--surprise.svg b/src/components/action-menu/icon--surprise.svg new file mode 100644 index 00000000000..41655999f67 --- /dev/null +++ b/src/components/action-menu/icon--surprise.svg @@ -0,0 +1,12 @@ + + + + surprise + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/asset-button/asset-button.css b/src/components/asset-button/asset-button.css deleted file mode 100644 index b36fa76b8ec..00000000000 --- a/src/components/asset-button/asset-button.css +++ /dev/null @@ -1,21 +0,0 @@ -@import "../../css/colors.css"; - -.container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - background: $motion-primary; - border-radius: 100%; - width: 2.75rem; - height: 2.75rem; - outline: none; - border: none; -} - -.icon { - width: 1.75rem; - height: 1.75rem; -} diff --git a/src/components/asset-button/asset-button.jsx b/src/components/asset-button/asset-button.jsx deleted file mode 100644 index 08883899094..00000000000 --- a/src/components/asset-button/asset-button.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; -import styles from './asset-button.css'; - -const AssetButton = ({ - img, - className, - title, - onClick -}) => ( - -); - -AssetButton.propTypes = { - className: PropTypes.string, - img: PropTypes.string, - onClick: PropTypes.func.isRequired, - title: PropTypes.node.isRequired -}; - -export default AssetButton; diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 2a19296d66a..24a5c2ec609 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -11,13 +11,30 @@ } .new-buttons { + position: absolute; + bottom: 0; + width: 100%; + display: flex; flex-direction: column; align-items: center; justify-content: space-around; - margin: 0.75rem 0; + padding: 0.75rem 0; color: $motion-primary; text-align: center; + background: none; +} + +$fade-out-distance: 100px; + +.new-buttons:before { + content: ""; + position: absolute; + bottom: 0; + left: 0; + background: linear-gradient(rgba(232,237,241, 0),rgba(232,237,241, 1)); + height: $fade-out-distance; + width: 100%; } .new-buttons > button + button { diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index b6970778155..f2ab58cae0d 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -4,7 +4,7 @@ import React from 'react'; import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx'; import Box from '../box/box.jsx'; -import AssetButton from '../asset-button/asset-button.jsx'; +import ActionMenu from '../action-menu/action-menu.jsx'; import styles from './selector.css'; const Selector = props => { @@ -17,6 +17,23 @@ const Selector = props => { onItemClick } = props; + let newButtonSection = null; + + if (buttons.length > 0) { + const {img, title, onClick} = buttons[0]; + const moreButtons = buttons.slice(1); + newButtonSection = ( + + + + ); + } + return ( @@ -35,16 +52,7 @@ const Selector = props => { /> ))} - - {buttons.map(({message, img, onClick}, index) => ( - - ))} - + {newButtonSection} ); }; diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css index 2c145283869..35ba2972638 100644 --- a/src/components/green-flag/green-flag.css +++ b/src/components/green-flag/green-flag.css @@ -5,6 +5,7 @@ padding: 0.375rem; border-radius: 0.25rem; user-select: none; + user-drag: none; cursor: pointer; transition: 0.2s ease-out; } diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 92b83acc2db..5132af685bf 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -140,7 +140,8 @@ For making the sprite-selector a scrollable pane @todo: Not working in Safari */ - overflow: hidden; + /* TODO this also breaks the thermometer menu */ + /* overflow: hidden; */ } .extension-button-container { @@ -156,6 +157,19 @@ box-sizing: content-box; /* To match scratch-block vertical toolbox borders */ } +$fade-out-distance: 15px; + +.extension-button-container:before { + content: ""; + position: absolute; + top: calc(calc(-1 * $fade-out-distance) - 1px); + left: -1px; + background: linear-gradient(rgba(0, 0, 0, 0),rgba(0, 0, 0, 0.15)); + height: $fade-out-distance; + width: calc(100% + 0.5px); +} + + .extension-button { background: none; border: none; diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css index 67b0532d394..4cb03ecd581 100644 --- a/src/components/sprite-selector/sprite-selector.css +++ b/src/components/sprite-selector/sprite-selector.css @@ -2,7 +2,7 @@ .sprite-selector { flex-grow: 1; - position: relative; + position: relative; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; margin-right: calc($space / 2); background-color: #f9f9f9; @@ -16,14 +16,14 @@ /* In prep for renaming sprite-selector-item to sprite */ .sprite { - /* + /* Our goal is to fit sprites evenly in a row without leftover space. - Flexbox's `space between` property gets us close, but doesn't flow + Flexbox's `space between` property gets us close, but doesn't flow well when the # of items per row > 1 and less than the max per row. - Solving by explicitly calc'ing the width of each sprite. Setting - `border-box` simplifies things, because content, padding and - border-width all are included in the width, leaving us only to subtract + Solving by explicitly calc'ing the width of each sprite. Setting + `border-box` simplifies things, because content, padding and + border-width all are included in the width, leaving us only to subtract the left + right margins. @todo: make room for the scrollbar @@ -32,7 +32,7 @@ width: calc((100% / $sprites-per-row ) - $space); min-width: 4rem; min-height: 4rem; /* @todo: calc height same as width */ - margin: calc($space / 2); + margin: calc($space / 2); } @@ -41,11 +41,11 @@ Sets the sprite-selector items as a scrollable pane @todo: Safari: pane doesn't stretch to fill height; - @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and - also introduces a new bug in Chrome when vertically resizing window down, - then back up, introduces white space in the outside the page container. + @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and + also introduces a new bug in Chrome when vertically resizing window down, + then back up, introduces white space in the outside the page container. */ - height: calc(100% - $sprite-info-height); + height: calc(100% - $sprite-info-height); overflow-y: scroll; } @@ -57,11 +57,12 @@ padding-top: calc($space / 2); padding-left: calc($space / 2); padding-right: calc($space / 2); - padding-bottom: $space; + padding-bottom: $space; } .add-button { position: absolute; bottom: 0.75rem; right: 1rem; + z-index: 1; /* TODO overlaps the stage, this doesn't work, fix! */ } diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index 41e51a31626..7590f286e5e 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -5,16 +5,41 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl'; import Box from '../box/box.jsx'; import SpriteInfo from '../../containers/sprite-info.jsx'; import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx'; -import AssetButton from '../asset-button/asset-button.jsx'; +import ActionMenu from '../action-menu/action-menu.jsx'; import styles from './sprite-selector.css'; -import spriteIcon from './icon--sprite.svg'; + +import cameraIcon from '../action-menu/icon--camera.svg'; +import fileUploadIcon from '../action-menu/icon--file-upload.svg'; +import paintIcon from '../action-menu/icon--paint.svg'; +import spriteIcon from '../action-menu/icon--sprite.svg'; +import surpriseIcon from '../action-menu/icon--surprise.svg'; const messages = defineMessages({ - addSprite: { - id: 'gui.spriteSelector.addSprite', - description: 'Button to add a sprite in the target pane', - defaultMessage: 'Add Sprite' + addSpriteFromLibrary: { + id: 'gui.spriteSelector.addSpriteFromLibrary', + description: 'Button to add a sprite in the target pane from library', + defaultMessage: 'Sprite Library' + }, + addSpriteFromPaint: { + id: 'gui.spriteSelector.addSpriteFromPaint', + description: 'Button to add a sprite in the target pane from paint', + defaultMessage: 'Paint' + }, + addSpriteFromSurprise: { + id: 'gui.spriteSelector.addSpriteFromSurprise', + description: 'Button to add a random sprite in the target pane', + defaultMessage: 'Surprise' + }, + addSpriteFromFile: { + id: 'gui.spriteSelector.addSpriteFromFile', + description: 'Button to add a sprite in the target pane from file', + defaultMessage: 'Coming Soon' + }, + addSpriteFromCamera: { + id: 'gui.spriteSelector.addSpriteFromCamera', + description: 'Button to add a sprite in the target pane from camera', + defaultMessage: 'Coming Soon' } }); @@ -30,6 +55,8 @@ const SpriteSelectorComponent = function (props) { onDeleteSprite, onDuplicateSprite, onNewSpriteClick, + onSurpriseSpriteClick, + onPaintSpriteClick, onSelectSprite, selectedId, sprites, @@ -85,10 +112,27 @@ const SpriteSelectorComponent = function (props) { } - @@ -106,7 +150,9 @@ SpriteSelectorComponent.propTypes = { onDeleteSprite: PropTypes.func, onDuplicateSprite: PropTypes.func, onNewSpriteClick: PropTypes.func, + onPaintSpriteClick: PropTypes.func, onSelectSprite: PropTypes.func, + onSurpriseSpriteClick: PropTypes.func, selectedId: PropTypes.string, sprites: PropTypes.shape({ id: PropTypes.shape({ diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx index eea81140946..073ca257c15 100644 --- a/src/components/stage-selector/stage-selector.jsx +++ b/src/components/stage-selector/stage-selector.jsx @@ -4,16 +4,41 @@ import React from 'react'; import {defineMessages, intlShape, injectIntl, FormattedMessage} from 'react-intl'; import Box from '../box/box.jsx'; -import AssetButton from '../asset-button/asset-button.jsx'; +import ActionMenu from '../action-menu/action-menu.jsx'; import CostumeCanvas from '../costume-canvas/costume-canvas.jsx'; import styles from './stage-selector.css'; -import backdropIcon from './icon--backdrop.svg'; + +import backdropIcon from '../action-menu/icon--backdrop.svg'; +import cameraIcon from '../action-menu/icon--camera.svg'; +import fileUploadIcon from '../action-menu/icon--file-upload.svg'; +import paintIcon from '../action-menu/icon--paint.svg'; +import surpriseIcon from '../action-menu/icon--surprise.svg'; const messages = defineMessages({ - addBackdrop: { - id: 'gui.stageSelector.targetPaneAddBackdrop', - description: 'Button to add a backdrop in the target pane', - defaultMessage: 'Add Backdrop' + addBackdropFromLibrary: { + id: 'gui.spriteSelector.addBackdropFromLibrary', + description: 'Button to add a stage in the target pane from library', + defaultMessage: 'Backdrop Library' + }, + addBackdropFromPaint: { + id: 'gui.stageSelector.addBackdropFromPaint', + description: 'Button to add a stage in the target pane from paint', + defaultMessage: 'Paint' + }, + addBackdropFromSurprise: { + id: 'gui.stageSelector.addBackdropFromSurprise', + description: 'Button to add a random stage in the target pane', + defaultMessage: 'Surprise' + }, + addBackdropFromFile: { + id: 'gui.stageSelector.addBackdropFromFile', + description: 'Button to add a stage in the target pane from file', + defaultMessage: 'Coming Soon' + }, + addBackdropFromCamera: { + id: 'gui.stageSelector.addBackdropFromCamera', + description: 'Button to add a stage in the target pane from camera', + defaultMessage: 'Coming Soon' } }); @@ -25,6 +50,8 @@ const StageSelector = props => { url, onClick, onNewBackdropClick, + onSurpriseBackdropClick, + onEmptyBackdropClick, ...componentProps } = props; return ( @@ -54,10 +81,28 @@ const StageSelector = props => { />
{backdropCount}
- @@ -68,8 +113,11 @@ StageSelector.propTypes = { backdropCount: PropTypes.number.isRequired, intl: intlShape.isRequired, onClick: PropTypes.func, + onEmptyBackdropClick: PropTypes.func, onNewBackdropClick: PropTypes.func, + onSurpriseBackdropClick: PropTypes.func, selected: PropTypes.bool.isRequired, url: PropTypes.string }; + export default injectIntl(StageSelector); diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index 30267688fe9..7369f07fc1e 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -29,6 +29,8 @@ const TargetPane = ({ onDeleteSprite, onDuplicateSprite, onNewSpriteClick, + onSurpriseSpriteClick, + onPaintSpriteClick, onRequestCloseSpriteLibrary, onRequestCloseBackdropLibrary, onSelectSprite, @@ -54,7 +56,9 @@ const TargetPane = ({ onDeleteSprite={onDeleteSprite} onDuplicateSprite={onDuplicateSprite} onNewSpriteClick={onNewSpriteClick} + onPaintSpriteClick={onPaintSpriteClick} onSelectSprite={onSelectSprite} + onSurpriseSpriteClick={onSurpriseSpriteClick} />
{stage.id && 2 ? item.info[2] : 1, + skinId: null + }; + this.props.vm.addCostume(item.md5, vmCostume).then(() => { + this.handleNewCostume(); + }); + } + handleSurpriseBackdrop () { + const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)]; + const vmCostume = { + name: item.name, + rotationCenterX: item.info[0] && item.info[0] / 2, + rotationCenterY: item.info[1] && item.info[1] / 2, + bitmapResolution: item.info.length > 2 ? item.info[2] : 1, + skinId: null + }; + this.props.vm.addCostume(item.md5, vmCostume).then(() => { + this.handleNewCostume(); + }); + } render () { // For paint wrapper const { @@ -120,9 +161,7 @@ class CostumeTab extends React.Component { onNewLibraryBackdropClick, onNewLibraryCostumeClick, costumeLibraryVisible, - backdropLibraryVisible, onRequestCloseCostumeLibrary, - onRequestCloseBackdropLibrary, ...props } = this.props; @@ -140,7 +179,7 @@ class CostumeTab extends React.Component { } const addLibraryMessage = target.isStage ? messages.addLibraryBackdropMsg : messages.addLibraryCostumeMsg; - const addBlankMessage = target.isStage ? messages.addBlankBackdropMsg : messages.addBlankCostumeMsg; + const addSurpriseFunc = target.isStage ? this.handleSurpriseBackdrop : this.handleSurpriseCostume; const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick; const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon; @@ -148,14 +187,27 @@ class CostumeTab extends React.Component { ) : null} - {backdropLibraryVisible ? ( - - ) : null} ); } @@ -233,9 +278,6 @@ const mapDispatchToProps = dispatch => ({ e.preventDefault(); dispatch(openCostumeLibrary()); }, - onRequestCloseBackdropLibrary: () => { - dispatch(closeBackdropLibrary()); - }, onRequestCloseCostumeLibrary: () => { dispatch(closeCostumeLibrary()); } diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index 9998fbb3e91..d036caa236d 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -7,7 +7,7 @@ import extensionLibraryContent from '../lib/libraries/extensions/index'; import analytics from '../lib/analytics'; import LibraryComponent from '../components/library/library.jsx'; -import extensionIcon from '../components/sprite-selector/icon--sprite.svg'; +import extensionIcon from '../components/action-menu/icon--sprite.svg'; class ExtensionLibrary extends React.PureComponent { constructor (props) { diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index 8686ae6f905..471ebe99e65 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -8,10 +8,15 @@ import AssetPanel from '../components/asset-panel/asset-panel.jsx'; import soundIcon from '../components/asset-panel/icon--sound.svg'; import addSoundFromLibraryIcon from '../components/asset-panel/icon--add-sound-lib.svg'; import addSoundFromRecordingIcon from '../components/asset-panel/icon--add-sound-record.svg'; +import fileUploadIcon from '../components/action-menu/icon--file-upload.svg'; +import surpriseIcon from '../components/action-menu/icon--surprise.svg'; + import RecordModal from './record-modal.jsx'; import SoundEditor from './sound-editor.jsx'; import SoundLibrary from './sound-library.jsx'; +import soundLibraryContent from '../lib/libraries/sounds.json'; + import {connect} from 'react-redux'; import { @@ -27,7 +32,8 @@ class SoundTab extends React.Component { 'handleSelectSound', 'handleDeleteSound', 'handleDuplicateSound', - 'handleNewSound' + 'handleNewSound', + 'handleSurpriseSound' ]); this.state = {selectedSoundIndex: 0}; } @@ -72,6 +78,20 @@ class SoundTab extends React.Component { this.setState({selectedSoundIndex: Math.max(sounds.length - 1, 0)}); } + handleSurpriseSound () { + const soundItem = soundLibraryContent[Math.floor(Math.random() * soundLibraryContent.length)]; + const vmSound = { + format: soundItem.format, + md5: soundItem.md5, + rate: soundItem.rate, + sampleCount: soundItem.sampleCount, + name: soundItem.name + }; + this.props.vm.addSound(vmSound).then(() => { + this.handleNewSound(); + }); + } + render () { const { intl, @@ -94,28 +114,45 @@ class SoundTab extends React.Component { )) : []; const messages = defineMessages({ + fileUploadSound: { + defaultMessage: 'Coming Soon', + description: 'Button to upload sound from file in the editor tab', + id: 'gui.soundTab.fileUploadSound' + }, + surpriseSound: { + defaultMessage: 'Surprise', + description: 'Button to get a random sound in the editor tab', + id: 'gui.soundTab.surpriseSound' + }, recordSound: { - defaultMessage: 'Record Sound', + defaultMessage: 'Record', description: 'Button to record a sound in the editor tab', id: 'gui.soundTab.recordSound' }, addSound: { - defaultMessage: 'Add Sound', + defaultMessage: 'Sound Library', description: 'Button to add a sound in the editor tab', - id: 'gui.soundTab.addSound' + id: 'gui.soundTab.addSoundFromLibrary' } }); return ( ({ url: soundIcon, diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx index 83a5a1bb06f..3d280bd1603 100644 --- a/src/containers/stage-selector.jsx +++ b/src/containers/stage-selector.jsx @@ -4,19 +4,51 @@ import React from 'react'; import {connect} from 'react-redux'; import {openBackdropLibrary} from '../reducers/modals'; +import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab'; + import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx'; +import backdropLibraryContent from '../lib/libraries/backdrops.json'; +import costumeLibraryContent from '../lib/libraries/costumes.json'; + class StageSelector extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleClick' + 'handleClick', + 'handleSurpriseBackdrop', + 'handleEmptyBackdrop', + 'addBackdropFromLibraryItem' ]); } + addBackdropFromLibraryItem (item) { + const vmBackdrop = { + name: item.name, + rotationCenterX: item.info[0] && item.info[0] / 2, + rotationCenterY: item.info[1] && item.info[1] / 2, + bitmapResolution: item.info.length > 2 ? item.info[2] : 1, + skinId: null + }; + return this.props.vm.addBackdrop(item.md5, vmBackdrop); + } handleClick (e) { e.preventDefault(); this.props.onSelect(this.props.id); } + handleSurpriseBackdrop () { + // @todo should this not add a backdrop you already have? + const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)]; + this.addBackdropFromLibraryItem(item); + } + handleEmptyBackdrop () { + // @todo this is brittle, will need to be refactored for localized libraries + const emptyItem = costumeLibraryContent.find(item => item.name === 'Empty'); + if (emptyItem) { + this.addBackdropFromLibraryItem(emptyItem).then(() => { + this.props.onActivateTab(COSTUMES_TAB_INDEX); + }); + } + } render () { const { /* eslint-disable no-unused-vars */ @@ -29,6 +61,8 @@ class StageSelector extends React.Component { return ( ); @@ -41,13 +75,17 @@ StageSelector.propTypes = { }; const mapStateToProps = (state, {assetId}) => ({ - url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI() + url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI(), + vm: state.vm }); const mapDispatchToProps = dispatch => ({ onNewBackdropClick: e => { e.preventDefault(); dispatch(openBackdropLibrary()); + }, + onActivateTab: tabIndex => { + dispatch(activateTab(tabIndex)); } }); diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 32ad448bc7d..a1665f80866 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -9,7 +9,10 @@ import { closeSpriteLibrary } from '../reducers/modals'; +import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab'; + import TargetPaneComponent from '../components/target-pane/target-pane.jsx'; +import spriteLibraryContent from '../lib/libraries/sprites.json'; class TargetPane extends React.Component { constructor (props) { @@ -23,7 +26,9 @@ class TargetPane extends React.Component { 'handleChangeSpriteY', 'handleDeleteSprite', 'handleDuplicateSprite', - 'handleSelectSprite' + 'handleSelectSprite', + 'handleSurpriseSpriteClick', + 'handlePaintSpriteClick' ]); } handleChangeSpriteDirection (direction) { @@ -53,6 +58,19 @@ class TargetPane extends React.Component { handleSelectSprite (id) { this.props.vm.setEditingTarget(id); } + handleSurpriseSpriteClick () { + const item = spriteLibraryContent[Math.floor(Math.random() * spriteLibraryContent.length)]; + this.props.vm.addSprite2(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.onActivateTab(COSTUMES_TAB_INDEX); + }); + } + } render () { return ( ); } @@ -105,6 +125,9 @@ const mapDispatchToProps = dispatch => ({ }, onRequestCloseBackdropLibrary: () => { dispatch(closeBackdropLibrary()); + }, + onActivateTab: tabIndex => { + dispatch(activateTab(tabIndex)); } }); diff --git a/src/css/colors.css b/src/css/colors.css index e270217f9a5..f4611c06e25 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -18,4 +18,6 @@ $control-primary: #FFAB19; $data-primary: #FF8C1A; +$pen-primary: #11B581; + $form-border: #E9EEF2; diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js index f07c3d0f309..00700cba2b4 100644 --- a/test/integration/costumes.test.js +++ b/test/integration/costumes.test.js @@ -29,7 +29,7 @@ describe('Working with costumes', () => { await loadUri(uri); await clickXpath('//button[@title="tryit"]'); await clickText('Costumes'); - await clickXpath('//button[@title="Add Costume"]'); + await clickXpath('//button[@aria-label="Costume Library"]'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); await el.sendKeys('abb'); await clickText('Abby-a'); // Should close the modal, then click the costumes in the selector @@ -57,7 +57,7 @@ describe('Working with costumes', () => { test('Adding a backdrop', async () => { await loadUri(uri); await clickXpath('//button[@title="tryit"]'); - await clickXpath('//button[@title="Add Backdrop"]'); + await clickXpath('//button[@aria-label="Backdrop Library"]'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); await el.sendKeys('blue'); await clickText('Blue Sky'); // Should close the modal diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index e3154868417..fc36c55a553 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -37,13 +37,13 @@ describe('Working with sounds', () => { .accept(); // Add it back - await clickXpath('//button[@title="Add Sound"]'); + await clickXpath('//button[@aria-label="Sound Library"]'); let el = await findByXpath("//input[@placeholder='what are you looking for?']"); await el.sendKeys('meow'); await clickText('Meow', scope.modal); // Should close the modal // Add a new sound - await clickXpath('//button[@title="Add Sound"]'); + await clickXpath('//button[@aria-label="Sound Library"]'); el = await findByXpath("//input[@placeholder='what are you looking for?']"); await el.sendKeys('chom'); await clickText('Chomp'); // Should close the modal, then click the sounds in the selector @@ -86,11 +86,11 @@ describe('Working with sounds', () => { // Add a sound so this sprite has 2 sounds. await clickText('Sounds'); - await clickXpath('//button[@title="Add Sound"]'); + await clickXpath('//button[@aria-label="Sound Library"]'); await clickText('A Bass'); // Closes the modal // Now add a sprite with only one sound. - await clickXpath('//button[@title="Add Sprite"]'); + await clickXpath('//button[@aria-label="Sprite Library"]'); await clickText('Abby'); // Doing this used to crash the editor. await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error