diff --git a/.travis.yml b/.travis.yml index 9290080d2..e429117fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ env: - NODE_ENV=production - SMOKE_URL=https://llk.github.io/scratch-gui/$TRAVIS_PULL_REQUEST_BRANCH - NPM_TAG=latest + - RELEASE_VERSION="0.1.0-prerelease.$(date +'%Y%m%d%H%M%S')" cache: directories: - node_modules @@ -22,8 +23,8 @@ script: before_deploy: - > if [ -z "$BEFORE_DEPLOY_RAN" ]; then - npm --no-git-tag-version version 0.1.0-prerelease.$(date +%Y%m%d%H%M%S) - if [ "$TRAVIS_BRANCH" == "develop" ]; then export NPM_TAG=develop; fi + npm --no-git-tag-version version $RELEASE_VERSION + if [ "$TRAVIS_BRANCH" == "master" ]; then export NPM_TAG=stable; fi git config --global user.email $(git log --pretty=format:"%ae" -n1) git config --global user.name $(git log --pretty=format:"%an" -n1) export BEFORE_DEPLOY_RAN=true @@ -40,6 +41,15 @@ deploy: email: $NPM_EMAIL api_key: $NPM_TOKEN tag: $NPM_TAG +- provider: script + on: + branch: + - master + - develop + - smoke + condition: $TRAVIS_EVENT_TYPE != cron + skip_cleanup: true + script: if npm info scratch-gui | grep -q $RELEASE_VERSION; then git tag $RELEASE_VERSION && git push https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git $RELEASE_VERSION; fi - provider: s3 on: branch: @@ -58,11 +68,6 @@ deploy: condition: $TRAVIS_EVENT_TYPE != cron skip_cleanup: true script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -- provider: script - on: - all_branches: true - condition: $TRAVIS_EVENT_TYPE != cron - script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git - provider: script on: branch: develop diff --git a/package.json b/package.json index 22c72b0bd..912ef7a27 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "lodash.throttle": "4.0.1", "minilog": "3.1.0", "mkdirp": "^0.5.1", + "omggif": "1.0.9", "papaparse": "4.6.2", "postcss-import": "^12.0.0", "postcss-loader": "^3.0.0", @@ -104,14 +105,14 @@ "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", "rimraf": "^2.6.1", - "scratch-audio": "0.1.0-prerelease.20190108181031", - "scratch-blocks": "0.1.0-prerelease.1545364693", - "scratch-l10n": "3.1.20181220222259", - "scratch-paint": "0.2.0-prerelease.20181220194927", - "scratch-render": "0.1.0-prerelease.20190109201300", + "scratch-audio": "0.1.0-prerelease.20190114210212", + "scratch-blocks": "0.1.0-prerelease.1549990124", + "scratch-l10n": "3.1.20190220143732", + "scratch-paint": "0.2.0-prerelease.20190114205252", + "scratch-render": "0.1.0-prerelease.20190213183713", "scratch-storage": "1.2.2", - "scratch-svg-renderer": "0.2.0-prerelease.20190109201344", - "scratch-vm": "0.2.0-prerelease.20190109200835", + "scratch-svg-renderer": "0.2.0-prerelease.20190125192231", + "scratch-vm": "0.2.0-prerelease.20190226023539", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx index 36cf5d444..f9e50f3ff 100644 --- a/src/components/action-menu/action-menu.jsx +++ b/src/components/action-menu/action-menu.jsx @@ -142,7 +142,7 @@ class ActionMenu extends React.Component {
{(moreButtons || []).map(({img, title, onClick: handleClick, - fileAccept, fileChange, fileInput}, keyId) => { + fileAccept, fileChange, fileInput, fileMultiple}, keyId) => { const isComingSoon = !handleClick; const hasFileInput = fileInput; const tooltipId = `${this.mainTooltipId}-${title}`; @@ -166,6 +166,7 @@ class ActionMenu extends React.Component { ( )} {props.playhead ? ( - +
+
+
) : null} {props.trimEnd === null ? null : ( diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css index ad7644c64..25bc42d45 100644 --- a/src/components/backpack/backpack.css +++ b/src/components/backpack/backpack.css @@ -69,9 +69,18 @@ } .backpack-item { - min-width: 4rem; - max-width: 6rem; + width: 4rem; + height: 4.5rem; margin: 0 0.25rem; + flex-shrink: 0; + + /* Need to hide overflow because of background setting below */ + overflow: hidden; +} + +.backpack-item > div { + /* Need to set the background to get blend-mode below to work */ + background: $ui-primary; } .backpack-item img { diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index fd66c68bd..583f587f7 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -1,5 +1,6 @@ @import "../../css/units.css"; @import "../../css/colors.css"; +@import "../../css/z-index.css"; .blocks { height: 100%; @@ -80,6 +81,7 @@ This does not prevent user interaction on the blocks themselves. */ pointer-events: none; + z-index: $z-index-drag-layer; /* make blocks match gui drag layer */ } /* diff --git a/src/components/cards/cards.jsx b/src/components/cards/cards.jsx index f137c91a0..79d2e6ef3 100644 --- a/src/components/cards/cards.jsx +++ b/src/components/cards/cards.jsx @@ -12,6 +12,7 @@ import helpIcon from '../../lib/assets/icon--tutorials.svg'; import closeIcon from './icon--close.svg'; import {translateVideo} from '../../lib/libraries/decks/translate-video.js'; +import {translateImage} from '../../lib/libraries/decks/translate-image.js'; const CardHeader = ({onCloseCards, onShowAll, totalSteps, step}) => (
@@ -275,7 +276,7 @@ const Cards = props => { /> ) : ( ) diff --git a/src/components/context-menu/context-menu.css b/src/components/context-menu/context-menu.css index 5a02ce797..84e2699f0 100644 --- a/src/components/context-menu/context-menu.css +++ b/src/components/context-menu/context-menu.css @@ -28,3 +28,11 @@ background: $motion-primary; color: white; } + +.menu-item-bordered { + border-top: 1px solid $ui-black-transparent; +} + +.menu-item-bordered:hover { + background: $error-primary; +} diff --git a/src/components/context-menu/context-menu.jsx b/src/components/context-menu/context-menu.jsx index 7d33fe641..677f89441 100644 --- a/src/components/context-menu/context-menu.jsx +++ b/src/components/context-menu/context-menu.jsx @@ -1,5 +1,6 @@ import React from 'react'; import {ContextMenu, MenuItem} from 'react-contextmenu'; +import classNames from 'classnames'; import styles from './context-menu.css'; @@ -17,7 +18,16 @@ const StyledMenuItem = props => ( /> ); +const BorderedMenuItem = props => ( + +); + + export { + BorderedMenuItem, StyledContextMenu as ContextMenu, StyledMenuItem as MenuItem }; diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css index 62b5ffe5f..aa4eb536d 100644 --- a/src/components/green-flag/green-flag.css +++ b/src/components/green-flag/green-flag.css @@ -8,11 +8,10 @@ user-select: none; user-drag: none; cursor: pointer; - transition: 0.2s ease-out; } .green-flag:hover { - transform: scale(1.2); + background-color: $motion-light-transparent; } .green-flag.is-active { diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index a203310b1..97741a147 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -23,8 +23,6 @@ import BackdropLibrary from '../../containers/backdrop-library.jsx'; import Watermark from '../../containers/watermark.jsx'; import Backpack from '../../containers/backpack.jsx'; -import PreviewModal from '../../containers/preview-modal.jsx'; -import ImportModal from '../../containers/import-modal.jsx'; import WebGlModal from '../../containers/webgl-modal.jsx'; import TipsLibrary from '../../containers/tips-library.jsx'; import Cards from '../../containers/cards.jsx'; @@ -80,9 +78,9 @@ const GUIComponent = props => { costumeLibraryVisible, costumesTabVisible, enableCommunity, - importInfoVisible, intl, isCreating, + isFullScreen, isPlayerOnly, isRtl, isShared, @@ -107,7 +105,6 @@ const GUIComponent = props => { onTelemetryModalCancel, onTelemetryModalOptIn, onTelemetryModalOptOut, - previewInfoVisible, showComingSoon, soundsTabVisible, stageSizeMode, @@ -139,6 +136,7 @@ const GUIComponent = props => { return isPlayerOnly ? ( { dir={isRtl ? 'rtl' : 'ltr'} {...componentProps} > - {previewInfoVisible ? ( - - ) : null} {telemetryModalVisible ? ( { {isCreating ? ( ) : null} - {importInfoVisible ? ( - - ) : null} {isRendererSupported ? null : ( )} @@ -335,6 +327,7 @@ const GUIComponent = props => { @@ -376,9 +369,9 @@ GUIComponent.propTypes = { costumeLibraryVisible: PropTypes.bool, costumesTabVisible: PropTypes.bool, enableCommunity: PropTypes.bool, - importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, isCreating: PropTypes.bool, + isFullScreen: PropTypes.bool, isPlayerOnly: PropTypes.bool, isRtl: PropTypes.bool, isShared: PropTypes.bool, @@ -403,7 +396,6 @@ GUIComponent.propTypes = { onTelemetryModalOptOut: PropTypes.func, onToggleLoginOpen: PropTypes.func, onUpdateProjectTitle: PropTypes.func, - previewInfoVisible: PropTypes.bool, renderLogin: PropTypes.func, showComingSoon: PropTypes.bool, soundsTabVisible: PropTypes.bool, diff --git a/src/components/icon-button/icon-button.css b/src/components/icon-button/icon-button.css index ebd7fd656..f58ee6f8d 100644 --- a/src/components/icon-button/icon-button.css +++ b/src/components/icon-button/icon-button.css @@ -5,16 +5,11 @@ flex-direction: column; align-items: center; cursor: pointer; - transition: 0.2s; font-size: 0.75rem; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: $motion-primary; } -.container:hover { - transform: scale(1.1); -} - .container + .container { margin-top: 1.25rem; } diff --git a/src/components/import-modal/import-modal.css b/src/components/import-modal/import-modal.css deleted file mode 100644 index 7da215c01..000000000 --- a/src/components/import-modal/import-modal.css +++ /dev/null @@ -1,190 +0,0 @@ -@import "../../css/colors.css"; -@import "../../css/units.css"; -@import "../../css/typography.css"; -@import "../../css/z-index.css"; - -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: $z-index-modal; - background-color: $ui-modal-overlay; -} - -.modal-content { - margin: 100px auto; - outline: none; - border: .25rem solid $ui-white-transparent; - padding: 0; - border-radius: $space; - user-select: none; - width: 500px; - color: $text-primary; - overflow: hidden; -} - -/* - TODO figure out how to remove filter altogether - since it is null... - Modal header has 3 items: - |x title filter| - - Use the same width for both side item containers, - so that title remains centered -*/ -$sides: 20rem; - -.header { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: flex-start; - height: $library-header-height; - - box-sizing: border-box; - width: 100%; - background-color: $extensions-primary; -} - -.header-item { - display: flex; - align-items: center; - padding: 1rem; - text-decoration: none; - color: white; - user-select: none; -} - -.header-item-filter { - display: flex; - flex-basis: $sides; - justify-content: flex-end; -} - -.header-item-title { - flex-grow: 1; - flex-shrink: 0; - justify-content: center; - user-select: none; - letter-spacing: 0.4px; - cursor: default; -} - -.header-item-title h2 { - font-size: 1.25rem; -} - -.header-item-close { - display: flex; - flex-basis: $sides; - justify-content: flex-start; -} - -[dir="rtl"] .header-item-close img { - transform: scaleX(-1); -} - -.body { - background: $ui-white; - padding: 1.5rem 2.25rem; - text-align: center; -} - -.input-row { - margin: 1.5rem 0; - font-weight: bolder; - text-align: right; - display: flex; - justify-content: center; - border: 1px solid; - border-radius: 0.25rem; - overflow: hidden; -} - -.ok-input-container { - border-color: $motion-primary; - box-shadow: 0 0 0 0.2rem $motion-transparent; -} - -.bad-input-container { - border-color: $data-primary; - box-shadow: 0 0 0 0.2rem hsla(30, 100%, 55%, 0.15); -} - -.input-row input { - width: 100%; - padding: 0 1rem; - height: 3rem; - color: $text-primary; - font-size: .875rem; - outline: none; - border: none; -} - -.input-row input::placeholder { - font-style: italic; - color: $text-primary-transparent; -} - -.input-row button { - padding: 0.5rem 2rem; - font-weight: bold; - font-size: .875rem; - cursor: pointer; - border: 0px solid $extensions-primary; - outline: none; -} - -.input-row button.ok-button { - background: $extensions-primary; - color: white; -} - -.error-row { - margin: 1.5rem 0; - text-align: center; - display: flex; - justify-content: center; - background: hsla(30, 100%, 55%, 0.25); - color: $data-primary; - border: 1px solid $data-primary; - border-radius: 0.25rem; -} - -.error-row p { - font-size: 0.875rem; - font-weight: bold; -} - -/* Confirmation buttons at the bottom of the modal */ -.button-row { - margin: 1.5rem 0; - font-weight: bolder; - text-align: right; - display: flex; - justify-content: center; -} - -.button-row button { - border: 1px solid $motion-primary; - border-radius: 0.25rem; - padding: 0.5rem 1.5rem; - background: white; - font-weight: bold; - font-size: .875rem; - cursor: pointer; - color: $motion-primary; -} - -.faq-link-text { - margin: 2rem 0 .5rem 0; - font-size: .875rem; - color: $text-primary; -} - -.faq-link { - color: $motion-primary; - text-decoration: none; -} diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx deleted file mode 100644 index 60b130212..000000000 --- a/src/components/import-modal/import-modal.jsx +++ /dev/null @@ -1,168 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ReactModal from 'react-modal'; -import Box from '../box/box.jsx'; -import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; -import classNames from 'classnames'; - -import CloseButton from '../close-button/close-button.jsx'; - -import styles from './import-modal.css'; - -const messages = defineMessages({ - title: { - id: 'gui.importInfo.title', - defaultMessage: 'View a Scratch 2.0 Project', - description: 'Scratch 2.0 import modal label - for accessibility' - }, - formDescription: { - defaultMessage: - 'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Beta will not be saved.', - description: 'Import project message', - id: 'gui.importInfo.betamessage' - }, - previewFormDescription: { - defaultMessage: - 'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Preview will not be saved.', - description: 'Import project message', - id: 'gui.importInfo.message' - }, - invalidFormatError: { - id: 'gui.importInfo.invalidFormatError', - defaultMessage: 'Uh oh, that project link or id doesn\'t look quite right.', - description: 'Invalid project link or id message' - } -}); - -const ImportModal = ({intl, ...props}) => ( - -
- -
-
- -
-
-

- {intl.formatMessage({...messages.title})} -

-
-
- {null} -
-
-
- - -

- {intl.formatMessage({...messages.formDescription})} -

- - - - - {props.hasValidationError ? - -

- -

-
: null - } - - - - - - - - ) - }} - /> - -
-
-
-); - -ImportModal.propTypes = { - errorMessage: PropTypes.string.isRequired, - hasValidationError: PropTypes.bool.isRequired, - inputValue: PropTypes.string.isRequired, - intl: intlShape.isRequired, - isRtl: PropTypes.bool, - onCancel: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onGoBack: PropTypes.func.isRequired, - onKeyPress: PropTypes.func.isRequired, - onViewProject: PropTypes.func.isRequired, - placeholder: PropTypes.string -}; - -export default injectIntl(ImportModal); diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 55edcc2e3..692e1b612 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -8,6 +8,7 @@ justify-content: flex-start; flex-basis: 160px; height: 160px; + max-width: 160px; margin: $space; padding: 1rem 1rem 0 1rem; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 4289a5269..9d14c562f 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -10,6 +10,7 @@ import classNames from 'classnames'; import bluetoothIconURL from './bluetooth.svg'; import internetConnectionIconURL from './internet-connection.svg'; +/* eslint-disable react/prefer-stateless-function */ class LibraryItemComponent extends React.PureComponent { render () { return this.props.featured ? ( @@ -132,6 +133,8 @@ class LibraryItemComponent extends React.PureComponent { ); } } +/* eslint-enable react/prefer-stateless-function */ + LibraryItemComponent.propTypes = { bluetoothRequired: PropTypes.bool, @@ -151,12 +154,12 @@ LibraryItemComponent.propTypes = { PropTypes.string, PropTypes.node ]), - onBlur: PropTypes.func, - onClick: PropTypes.func, - onFocus: PropTypes.func, - onKeyPress: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func + onBlur: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + onFocus: PropTypes.func.isRequired, + onKeyPress: PropTypes.func.isRequired, + onMouseEnter: PropTypes.func.isRequired, + onMouseLeave: PropTypes.func.isRequired }; LibraryItemComponent.defaultProps = { diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index d12295f5f..c7359dd1a 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -52,11 +52,9 @@ class LibraryComponent extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleBlur', 'handleClose', 'handleFilterChange', 'handleFilterClear', - 'handleFocus', 'handleMouseEnter', 'handleMouseLeave', 'handleSelect', @@ -75,12 +73,6 @@ class LibraryComponent extends React.Component { this.scrollToTop(); } } - handleBlur (id) { - this.handleMouseLeave(id); - } - handleFocus (id) { - this.handleMouseEnter(id); - } handleSelect (id) { this.handleClose(); this.props.onItemSelected(this.getFilteredData()[id]); @@ -210,13 +202,12 @@ class LibraryComponent extends React.Component { featured={dataItem.featured} hidden={dataItem.hidden} iconSource={iconSource} + icons={dataItem.json && dataItem.json.costumes} id={index} insetIconURL={dataItem.insetIconURL} internetConnectionRequired={dataItem.internetConnectionRequired} key={`item_${index}`} name={dataItem.name} - onBlur={this.handleBlur} - onFocus={this.handleFocus} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onSelect={this.handleSelect} diff --git a/src/components/loader/loader.css b/src/components/loader/loader.css index a1f0003c3..b57aa4c3a 100644 --- a/src/components/loader/loader.css +++ b/src/components/loader/loader.css @@ -17,6 +17,13 @@ color: white; } +.fullscreen { + /* Break out of the layout using position: fixed to cover the whole screen */ + position: fixed; + /* Use the fullscreen stage z-index to allow covering full-screen mode */ + z-index: $z-index-stage-wrapper-overlay; +} + .block-animation { width: 125px; height: 150px; diff --git a/src/components/loader/loader.jsx b/src/components/loader/loader.jsx index e06b8bb35..c51621fc4 100644 --- a/src/components/loader/loader.jsx +++ b/src/components/loader/loader.jsx @@ -1,5 +1,6 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; +import classNames from 'classnames'; import styles from './loader.css'; import PropTypes from 'prop-types'; @@ -119,15 +120,13 @@ class LoaderComponent extends React.Component { constructor (props) { super(props); this.state = { - messageNumber: 0 + messageNumber: this.chooseRandomMessage() }; } componentDidMount () { - this.chooseRandomMessage(); - // Start an interval to choose a new message every 5 seconds this.intervalId = setInterval(() => { - this.chooseRandomMessage(); + this.setState({messageNumber: this.chooseRandomMessage()}); }, 5000); } componentWillUnmount () { @@ -144,11 +143,15 @@ class LoaderComponent extends React.Component { break; } } - this.setState({messageNumber}); + return messageNumber; } render () { return ( -
+
{ const barHeight = (height - (barSpacing * (nBars + 1))) / nBars; const nBarsToMask = nBars - Math.floor(level * nBars); + const scale = ((nBarsToMask * (barHeight + barSpacing)) + (barSpacing / 2)) / height; return ( - - {Array(nBars).fill(0) - .map((value, index) => ( - - ))} - + {Array(nBars).fill(0) + .map((value, index) => ( + + ))} + +
- +
); }; diff --git a/src/components/monitor-list/monitor-list.jsx b/src/components/monitor-list/monitor-list.jsx index f7b02080f..e4da715b0 100644 --- a/src/components/monitor-list/monitor-list.jsx +++ b/src/components/monitor-list/monitor-list.jsx @@ -27,6 +27,7 @@ const MonitorList = props => ( draggable={props.draggable} height={monitorData.height} id={monitorData.id} + isDiscrete={monitorData.isDiscrete} key={monitorData.id} max={monitorData.sliderMax} min={monitorData.sliderMin} diff --git a/src/components/monitor/slider-monitor.jsx b/src/components/monitor/slider-monitor.jsx index b9cebfa14..0271b140a 100644 --- a/src/components/monitor/slider-monitor.jsx +++ b/src/components/monitor/slider-monitor.jsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import styles from './monitor.css'; -const SliderMonitor = ({categoryColor, label, min, max, value, onSliderUpdate}) => ( +const SliderMonitor = ({categoryColor, isDiscrete, label, min, max, value, onSliderUpdate}) => (
@@ -22,6 +22,7 @@ const SliderMonitor = ({categoryColor, label, min, max, value, onSliderUpdate}) className={classNames(styles.slider, 'no-drag')} // Class used on parent Draggable to prevent drags max={max} min={min} + step={isDiscrete ? 1 : 0.01} type="range" value={value} onChange={onSliderUpdate} @@ -33,6 +34,7 @@ const SliderMonitor = ({categoryColor, label, min, max, value, onSliderUpdate}) SliderMonitor.propTypes = { categoryColor: PropTypes.string.isRequired, + isDiscrete: PropTypes.bool, label: PropTypes.string.isRequired, max: PropTypes.number, min: PropTypes.number, @@ -44,6 +46,7 @@ SliderMonitor.propTypes = { }; SliderMonitor.defaultProps = { + isDiscrete: true, min: 0, max: 100 }; diff --git a/src/components/preview-modal/happy-cat.png b/src/components/preview-modal/happy-cat.png deleted file mode 100644 index 764761eea..000000000 Binary files a/src/components/preview-modal/happy-cat.png and /dev/null differ diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css deleted file mode 100644 index 6e17cb6ea..000000000 --- a/src/components/preview-modal/preview-modal.css +++ /dev/null @@ -1,108 +0,0 @@ -@import "../../css/colors.css"; -@import "../../css/units.css"; -@import "../../css/typography.css"; -@import "../../css/z-index.css"; - -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: $z-index-modal; - background-color: $ui-modal-overlay; -} - -.modal-content { - margin: 100px auto; - outline: none; - border: .25rem solid $ui-white-transparent; - padding: 0; - border-radius: $space; - user-select: none; - width: 500px; - - color: $text-primary; - overflow: hidden; -} - -.illustration { - width: 100%; - height: 208px; - background-color: $motion-primary; - background-image: url('./welcome.png'); - background-size: cover; -} - -.body { - background: $ui-white; - padding: 1.5rem 2.25rem; - text-align: center; -} - -/* Confirmation buttons at the bottom of the modal */ -.button-row { - margin: 1.5rem 0; - font-weight: bolder; - text-align: right; - display: flex; - justify-content: center; -} - -.button-row button { - border: 1px solid $motion-primary; - border-radius: 0.25rem; - padding: 0.5rem 1.5rem; - background: white; - font-weight: bold; - font-size: .875rem; - cursor: pointer; -} - -.button-row button.ok-button { - background: $motion-primary; - color: white; -} - -.button-row button.no-button { - color: $motion-primary; -} - -.button-row button.view-project-button { - background: $extensions-primary; - border-color: $extensions-primary; - color: white; -} - -[dir="ltr"] .button-row button + button { - margin-left: 0.5rem; -} - -[dir="rtl"] .button-row button + button { - margin-right: 0.5rem; -} - -.cat-icon { - width: 1.5rem; - height: 1.5rem; - vertical-align: middle; -} - -[dir="ltr"] .cat-icon { - margin-left: .125rem; -} - -[dir="rtl"] .cat-icon { - margin-right: .125rem; -} - -.faq-link-text { - margin: 2rem 0 .5rem 0; - font-size: .875rem; - color: $text-primary; -} - -.faq-link { - color: $motion-primary; - text-decoration: none; -} diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx deleted file mode 100644 index babfc0a27..000000000 --- a/src/components/preview-modal/preview-modal.jsx +++ /dev/null @@ -1,144 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ReactModal from 'react-modal'; -import Box from '../box/box.jsx'; -import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; - -import styles from './preview-modal.css'; -import catIcon from './happy-cat.png'; - -const messages = defineMessages({ - label: { - id: 'gui.previewInfo.label', - defaultMessage: 'Try Scratch 3.0', - description: 'Scratch 3.0 modal label - for accessibility' - }, - previewWelcome: { - defaultMessage: 'Welcome to the Scratch 3.0 Beta', - description: 'Header for Preview Info Modal', - id: 'gui.previewInfo.welcome' - }, - notNowTooltip: { - defaultMessage: 'Not Now', - description: 'Tooltip for Not Now button', - id: 'gui.previewModal.notnowtooltip' - }, - tryItTooltip: { - defaultMessage: 'Try It', - description: 'Tooltip for Try It button', - id: 'gui.previewModal.tryittooltip' - }, - viewProjectTooltip: { - defaultMessage: 'View 2.0 Project', - description: 'Tooltip for View 2.0 Project button', - id: 'gui.previewModal.viewprojecttooltip' - } -}); - -const PreviewModal = ({intl, ...props}) => ( - -
- - - -

- -

-

- { /* eslint-disable max-len */ } - - { /* eslint-enable max-len */ } -

- - - - - - - - - - - ) - }} - /> - -
-
-
-); - -PreviewModal.propTypes = { - intl: intlShape.isRequired, - isRtl: PropTypes.bool, - onCancel: PropTypes.func.isRequired, - onTryIt: PropTypes.func.isRequired, - onViewProject: PropTypes.func.isRequired -}; - -export default injectIntl(PreviewModal); diff --git a/src/components/preview-modal/welcome.png b/src/components/preview-modal/welcome.png deleted file mode 100644 index 1172df932..000000000 Binary files a/src/components/preview-modal/welcome.png and /dev/null differ diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx index e9090bfe3..09c94d016 100644 --- a/src/components/sprite-info/sprite-info.jsx +++ b/src/components/sprite-info/sprite-info.jsx @@ -34,14 +34,15 @@ class SpriteInfo extends React.Component { shouldComponentUpdate (nextProps) { return ( this.props.rotationStyle !== nextProps.rotationStyle || - this.props.direction !== nextProps.direction || this.props.disabled !== nextProps.disabled || this.props.name !== nextProps.name || - this.props.size !== nextProps.size || this.props.stageSize !== nextProps.stageSize || this.props.visible !== nextProps.visible || - this.props.x !== nextProps.x || - this.props.y !== nextProps.y + // Only update these if rounded value has changed + Math.round(this.props.direction) !== Math.round(nextProps.direction) || + Math.round(this.props.size) !== Math.round(nextProps.size) || + Math.round(this.props.x) !== Math.round(nextProps.x) || + Math.round(this.props.y) !== Math.round(nextProps.y) ); } render () { @@ -110,7 +111,7 @@ class SpriteInfo extends React.Component { placeholder="x" tabIndex="0" type="text" - value={this.props.disabled ? '' : this.props.x} + value={this.props.disabled ? '' : Math.round(this.props.x)} onSubmit={this.props.onChangeX} /> @@ -137,7 +138,7 @@ class SpriteInfo extends React.Component { placeholder="y" tabIndex="0" type="text" - value={this.props.disabled ? '' : this.props.y} + value={this.props.disabled ? '' : Math.round(this.props.y)} onSubmit={this.props.onChangeY} /> @@ -237,14 +238,14 @@ class SpriteInfo extends React.Component { label={sizeLabel} tabIndex="0" type="text" - value={this.props.disabled ? '' : this.props.size} + value={this.props.disabled ? '' : Math.round(this.props.size)} onSubmit={this.props.onChangeSize} />
( disable={props.dragging} id={`${props.name}-${contextMenuId}`} > - {(props.selected && props.onDeleteButtonClick) ? ( - - ) : null } {typeof props.number === 'undefined' ? null : (
{props.number}
)} {props.costumeURL ? ( - +
+
+ +
+
) : null}
{props.name}
@@ -49,6 +46,13 @@ const SpriteSelectorItem = props => (
{props.details}
) : null}
+ {(props.selected && props.onDeleteButtonClick) ? ( + + ) : null } {props.onDuplicateButtonClick || props.onDeleteButtonClick || props.onExportButtonClick ? ( {props.onDuplicateButtonClick ? ( @@ -60,15 +64,6 @@ const SpriteSelectorItem = props => ( /> ) : null} - {props.onDeleteButtonClick ? ( - - - - ) : null } {props.onExportButtonClick ? ( ( /> ) : null } + {props.onDeleteButtonClick ? ( + + + + ) : null } ) : null} diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx index de727969e..e89898e47 100644 --- a/src/components/sprite-selector/sprite-list.jsx +++ b/src/components/sprite-selector/sprite-list.jsx @@ -8,9 +8,12 @@ import Box from '../box/box.jsx'; import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx'; import SortableHOC from '../../lib/sortable-hoc.jsx'; import SortableAsset from '../asset-panel/sortable-asset.jsx'; +import ThrottledPropertyHOC from '../../lib/throttled-property-hoc.jsx'; import styles from './sprite-selector.css'; +const ThrottledSpriteSelectorItem = ThrottledPropertyHOC('asset', 500)(SpriteSelectorItem); + const SpriteList = function (props) { const { containerRef, @@ -74,13 +77,13 @@ const SpriteList = function (props) { onAddSortable={onAddSortable} onRemoveSortable={onRemoveSortable} > - { title: intl.formatMessage(messages.addBackdropFromFile), img: fileUploadIcon, onClick: onBackdropFileUploadClick, - fileAccept: '.svg, .png, .jpg, .jpeg', // Bitmap coming soon + fileAccept: '.svg, .png, .jpg, .jpeg, .gif', fileChange: onBackdropFileUpload, - fileInput: fileInputRef + fileInput: fileInputRef, + fileMultiple: true }, { title: intl.formatMessage(messages.addBackdropFromSurprise), img: surpriseIcon, diff --git a/src/components/stage-wrapper/stage-wrapper.jsx b/src/components/stage-wrapper/stage-wrapper.jsx index 821e2c001..77e9f5595 100644 --- a/src/components/stage-wrapper/stage-wrapper.jsx +++ b/src/components/stage-wrapper/stage-wrapper.jsx @@ -12,6 +12,7 @@ import styles from './stage-wrapper.css'; const StageWrapperComponent = function (props) { const { + isFullScreen, isRtl, isRendererSupported, loading, @@ -41,15 +42,16 @@ const StageWrapperComponent = function (props) { } {loading ? ( - + ) : null} ); }; StageWrapperComponent.propTypes = { + isFullScreen: PropTypes.bool, isRendererSupported: PropTypes.bool.isRequired, - isRtl: PropTypes.bool, + isRtl: PropTypes.bool.isRequired, loading: PropTypes.bool, stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, vm: PropTypes.instanceOf(VM).isRequired diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index 6ceb4c7e9..19496c09b 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -128,6 +128,7 @@ to adjust for the border using a different method */ } .question-wrapper { + z-index: $z-index-stage-question; pointer-events: auto; } @@ -147,6 +148,8 @@ to adjust for the border using a different method */ align-items: center; background: rgba(0,0,0,0.25); border-radius: 0.5rem; + pointer-events: all; + cursor: pointer; } .green-flag-overlay { @@ -154,8 +157,6 @@ to adjust for the border using a different method */ border-radius: 100%; background: rgba(255,255,255,0.75); border: 3px solid $ui-white; - pointer-events: all; - cursor: pointer; display: flex; justify-content: center; align-items: center; diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx index 1a2837360..cf81f4ec2 100644 --- a/src/components/stage/stage.jsx +++ b/src/components/stage/stage.jsx @@ -81,11 +81,10 @@ const StageComponent = props => { /> {isStarted ? null : ( - - - + )} {isColorPicking && colorInfo ? ( diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css index b2d878582..da0bcc7b3 100644 --- a/src/components/stop-all/stop-all.css +++ b/src/components/stop-all/stop-all.css @@ -1,3 +1,5 @@ +@import "../../css/colors.css"; + .stop-all { width: 2rem; height: 2rem; @@ -5,17 +7,16 @@ border-radius: 0.25rem; user-select: none; cursor: pointer; - transition: 0.2s ease-out; +} + +.stop-all:hover { + background-color: $motion-light-transparent; } .stop-all { opacity: 0.5; } -.stop-all:hover { - transform: scale(1.2); -} - .stop-all.is-active { opacity: 1; } diff --git a/src/components/waveform/waveform.jsx b/src/components/waveform/waveform.jsx index 1b2abe96e..48a0a4935 100644 --- a/src/components/waveform/waveform.jsx +++ b/src/components/waveform/waveform.jsx @@ -2,52 +2,65 @@ import React from 'react'; import PropTypes from 'prop-types'; import styles from './waveform.css'; -const Waveform = props => { - const { - width, - height, - data - } = props; +// Waveform is expensive to compute, make sure it only updates when data does +// by using PureComponent. In future can be changed back to function with React.memo +// eslint-disable-next-line react/prefer-stateless-function +class Waveform extends React.PureComponent { + render () { + const { + width, + height, + data + } = this.props; - const cappedData = [0, ...data, 0]; + // Never want a density of points higher than the number of pixels + // This is very conservative, could be far fewer points because of curve smoothing. + // Drawing too many points seems to cause an explosion in browser + // composite time when animating the playhead + const takeEveryN = Math.ceil(data.length / width); - const points = [ - ...cappedData.map((v, i) => - [width * i / cappedData.length, height * v / 2] - ), - ...cappedData.reverse().map((v, i) => - [width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2] - ) - ]; + const filteredData = takeEveryN === 1 ? data : + data.filter((_, i) => i % takeEveryN === 0); - const pathComponents = points.map(([x, y], i) => { - const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0]; - return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`; - }); + const cappedData = [0, ...filteredData, 0]; - return ( - - - - + [width * i / cappedData.length, height * v / 2] + ), + ...cappedData.reverse().map((v, i) => + [width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2] + ) + ]; + const pathComponents = points.map(([x, y], i) => { + const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0]; + return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`; + }); + + return ( + + - - - ); -}; + + + + + ); + } +} Waveform.propTypes = { data: PropTypes.arrayOf(PropTypes.number), diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index e6eebec41..ecb998ea4 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -50,6 +50,7 @@ class Blocks extends React.Component { bindAll(this, [ 'attachVM', 'detachVM', + 'getToolboxXML', 'handleCategorySelected', 'handleConnectionModalStart', 'handleDrop', @@ -86,6 +87,7 @@ class Blocks extends React.Component { componentDidMount () { this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; + this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, @@ -94,6 +96,11 @@ class Blocks extends React.Component { ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); + // Store the xml of the toolbox that is actually rendered. + // This is used in componentDidUpdate instead of prevProps, because + // the xml can change while e.g. on the costumes tab. + this._renderedToolboxXML = this.props.toolboxXML; + // we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the // entire toolbox every time we reset the workspace. We call updateToolbox as a part of // componentDidUpdate so the toolbox will still correctly be updated @@ -117,7 +124,7 @@ class Blocks extends React.Component { return ( this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible || - this.props.toolboxXML !== nextProps.toolboxXML || + this._renderedToolboxXML !== nextProps.toolboxXML || this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible || this.props.customProceduresVisible !== nextProps.customProceduresVisible || this.props.locale !== nextProps.locale || @@ -131,13 +138,13 @@ class Blocks extends React.Component { this.ScratchBlocks.hideChaff(); } - if (prevProps.toolboxXML !== this.props.toolboxXML) { - // rather than update the toolbox "sync" -- update it in the next frame - clearTimeout(this.toolboxUpdateTimeout); - this.toolboxUpdateTimeout = setTimeout(() => { - this.updateToolbox(); - }, 0); + // Only rerender the toolbox when the blocks are visible and the xml is + // different from the previously rendered toolbox xml. + // Do not check against prevProps.toolboxXML because that may not have been rendered. + if (this.props.isVisible && this.props.toolboxXML !== this._renderedToolboxXML) { + this.requestToolboxUpdate(); } + if (this.props.isVisible === prevProps.isVisible) { if (this.props.stageSize !== prevProps.stageSize) { // force workspace to redraw for the new stage size @@ -155,7 +162,7 @@ class Blocks extends React.Component { this.setLocale(); } else { this.props.vm.refreshWorkspace(); - this.updateToolbox(); + this.requestToolboxUpdate(); } window.dispatchEvent(new Event('resize')); @@ -168,15 +175,22 @@ class Blocks extends React.Component { this.workspace.dispose(); clearTimeout(this.toolboxUpdateTimeout); } - + requestToolboxUpdate () { + clearTimeout(this.toolboxUpdateTimeout); + this.toolboxUpdateTimeout = setTimeout(() => { + this.updateToolbox(); + }, 0); + } setLocale () { - this.workspace.getFlyout().setRecyclingEnabled(false); this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); this.props.vm.setLocale(this.props.locale, this.props.messages) .then(() => { + this.workspace.getFlyout().setRecyclingEnabled(false); this.props.vm.refreshWorkspace(); - this.updateToolbox(); - this.workspace.getFlyout().setRecyclingEnabled(true); + this.requestToolboxUpdate(); + this.withToolboxUpdates(() => { + this.workspace.getFlyout().setRecyclingEnabled(true); + }); }); } @@ -186,6 +200,8 @@ class Blocks extends React.Component { const categoryId = this.workspace.toolbox_.getSelectedCategoryId(); const offset = this.workspace.toolbox_.getCategoryScrollOffset(); this.workspace.updateToolbox(this.props.toolboxXML); + this._renderedToolboxXML = this.props.toolboxXML; + // In order to catch any changes that mutate the toolbox during "normal runtime" // (variable changes/etc), re-enable toolbox refresh. // Using the setter function will rerender the entire toolbox which we just rendered. @@ -230,7 +246,7 @@ class Blocks extends React.Component { this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded); this.props.vm.addListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.addListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate); + this.props.vm.addListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); } detachVM () { this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); @@ -243,7 +259,7 @@ class Blocks extends React.Component { this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded); this.props.vm.removeListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate); this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); - this.props.vm.removeListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate); + this.props.vm.removeListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); } updateToolboxBlockValue (id, value) { @@ -259,7 +275,7 @@ class Blocks extends React.Component { } onTargetsUpdate () { - if (this.props.vm.editingTarget) { + if (this.props.vm.editingTarget && this.workspace.getFlyout()) { ['glide', 'move', 'set'].forEach(prefix => { this.updateToolboxBlockValue(`${prefix}x`, Math.round(this.props.vm.editingTarget.x).toString()); this.updateToolboxBlockValue(`${prefix}y`, Math.round(this.props.vm.editingTarget.y).toString()); @@ -294,12 +310,32 @@ class Blocks extends React.Component { onVisualReport (data) { this.workspace.reportValue(data.id, data.value); } + getToolboxXML () { + // Use try/catch because this requires digging pretty deep into the VM + // Code inside intentionally ignores several error situations (no stage, etc.) + // Because they would get caught by this try/catch + try { + let {editingTarget: target, runtime} = this.props.vm; + const stage = runtime.getTargetForStage(); + if (!target) target = stage; // If no editingTarget, use the stage + + const stageCostumes = stage.getCostumes(); + const targetCostumes = target.getCostumes(); + const targetSounds = target.getSounds(); + const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); + return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML, + targetCostumes[0].name, + stageCostumes[0].name, + targetSounds.length > 0 ? targetSounds[0].name : '' + ); + } catch { + return null; + } + } onWorkspaceUpdate (data) { // When we change sprites, update the toolbox to have the new sprite's blocks - if (this.props.vm.editingTarget) { - const target = this.props.vm.editingTarget; - const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); - const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML); + const toolboxXML = this.getToolboxXML(); + if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } @@ -347,12 +383,9 @@ class Blocks extends React.Component { // this actually defines blocks and MUST run regardless of the UI state this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x)); - // update the toolbox view: this can be skipped if we're not looking at a target, etc. - const runtime = this.props.vm.runtime; - const target = runtime.getEditingTarget() || runtime.getTargetForStage(); - if (target) { - const dynamicBlocksXML = runtime.getBlocksXML(); - const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML); + // Update the toolbox with new blocks + const toolboxXML = this.getToolboxXML(); + if (toolboxXML) { this.props.updateToolboxState(toolboxXML); } } @@ -423,6 +456,7 @@ class Blocks extends React.Component { .then(blocks => this.props.vm.shareBlocksToTarget(blocks, this.props.vm.editingTarget.id)) .then(() => { this.props.vm.refreshWorkspace(); + this.updateToolbox(); // To show new variables/custom blocks }); } render () { @@ -495,7 +529,7 @@ Blocks.propTypes = { extensionLibraryVisible: PropTypes.bool, isRtl: PropTypes.bool, isVisible: PropTypes.bool, - locale: PropTypes.string, + locale: PropTypes.string.isRequired, messages: PropTypes.objectOf(PropTypes.string), onActivateColorPicker: PropTypes.func, onActivateCustomProcedures: PropTypes.func, diff --git a/src/containers/cards.jsx b/src/containers/cards.jsx index 0cc39f5c5..e75b1e129 100644 --- a/src/containers/cards.jsx +++ b/src/containers/cards.jsx @@ -1,4 +1,6 @@ import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import React from 'react'; import { activateDeck, @@ -15,6 +17,29 @@ import { } from '../reducers/modals'; import CardsComponent from '../components/cards/cards.jsx'; +import {loadImageData} from '../lib/libraries/decks/translate-image.js'; + +class Cards extends React.Component { + componentDidMount () { + if (this.props.locale !== 'en') { + loadImageData(this.props.locale); + } + } + componentDidUpdate (prevProps) { + if (this.props.locale !== prevProps.locale) { + loadImageData(this.props.locale); + } + } + render () { + return ( + + ); + } +} + +Cards.propTypes = { + locale: PropTypes.string.isRequired +}; const mapStateToProps = state => ({ visible: state.scratchGui.cards.visible, @@ -45,4 +70,4 @@ const mapDispatchToProps = dispatch => ({ export default connect( mapStateToProps, mapDispatchToProps -)(CardsComponent); +)(Cards); diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index 5df459150..6be2cf268 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -13,6 +13,7 @@ import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import DragConstants from '../lib/drag-constants'; import {emptyCostume} from '../lib/empty-assets'; import sharedMessages from '../lib/shared-messages'; +import downloadBlob from '../lib/download-blob'; import { closeCameraCapture, @@ -27,6 +28,7 @@ import { } from '../reducers/editor-tab'; import {setRestore} from '../reducers/restore-deletion'; +import {showStandardAlert, closeAlertWithId} from '../reducers/alerts'; import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg'; import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg'; @@ -86,6 +88,7 @@ class CostumeTab extends React.Component { 'handleSelectCostume', 'handleDeleteCostume', 'handleDuplicateCostume', + 'handleExportCostume', 'handleNewCostume', 'handleNewBlankCostume', 'handleSurpriseCostume', @@ -150,12 +153,20 @@ class CostumeTab extends React.Component { handleDuplicateCostume (costumeIndex) { this.props.vm.duplicateCostume(costumeIndex); } + handleExportCostume (costumeIndex) { + const item = this.props.vm.editingTarget.sprite.costumes[costumeIndex]; + const blob = new Blob([item.asset.data], {type: item.asset.assetType.contentType}); + downloadBlob(`${item.name}.${item.asset.dataFormat}`, blob); + } handleNewCostume (costume, fromCostumeLibrary) { - if (fromCostumeLibrary) { - this.props.vm.addCostumeFromLibrary(costume.md5, costume); - } else { - this.props.vm.addCostume(costume.md5, costume); - } + const costumes = Array.isArray(costume) ? costume : [costume]; + + return Promise.all(costumes.map(c => { + if (fromCostumeLibrary) { + return this.props.vm.addCostumeFromLibrary(c.md5, c); + } + return this.props.vm.addCostume(c.md5, c); + })); } handleNewBlankCostume () { const name = this.props.vm.editingTarget.isStage ? @@ -193,14 +204,26 @@ class CostumeTab extends React.Component { } handleCostumeUpload (e) { const storage = this.props.vm.runtime.storage; - handleFileUpload(e.target, (buffer, fileType, fileName) => { - costumeUpload(buffer, fileType, fileName, storage, this.handleNewCostume); - }); + this.props.onShowImporting(); + handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => { + costumeUpload(buffer, fileType, storage, vmCostumes => { + vmCostumes.forEach((costume, i) => { + costume.name = `${fileName}${i ? i + 1 : ''}`; + }); + this.handleNewCostume(vmCostumes).then(() => { + if (fileIndex === fileCount - 1) { + this.props.onCloseImporting(); + } + }); + }, this.props.onCloseImporting); + }, this.props.onCloseImporting); } handleCameraBuffer (buffer) { const storage = this.props.vm.runtime.storage; - const name = this.props.intl.formatMessage(messages.costume, {index: 1}); - costumeUpload(buffer, 'image/png', name, storage, this.handleNewCostume); + costumeUpload(buffer, 'image/png', storage, vmCostumes => { + vmCostumes[0].name = this.props.intl.formatMessage(messages.costume, {index: 1}); + this.handleNewCostume(vmCostumes); + }); } handleFileUploadClick () { this.fileInput.click(); @@ -284,9 +307,10 @@ class CostumeTab extends React.Component { title: intl.formatMessage(addFileMessage), img: fileUploadIcon, onClick: this.handleFileUploadClick, - fileAccept: '.svg, .png, .jpg, .jpeg', + fileAccept: '.svg, .png, .jpg, .jpeg, .gif', fileChange: this.handleCostumeUpload, - fileInput: this.setFileInput + fileInput: this.setFileInput, + fileMultiple: true }, { title: intl.formatMessage(messages.addSurpriseCostumeMsg), @@ -312,6 +336,7 @@ class CostumeTab extends React.Component { this.handleDeleteCostume : null} onDrop={this.handleDrop} onDuplicateClick={this.handleDuplicateCostume} + onExportClick={this.handleExportCostume} onItemClick={this.handleSelectCostume} > {target.costumes ? @@ -338,10 +363,12 @@ CostumeTab.propTypes = { intl: intlShape, isRtl: PropTypes.bool, onActivateSoundsTab: PropTypes.func.isRequired, + onCloseImporting: PropTypes.func.isRequired, onNewCostumeFromCameraClick: PropTypes.func.isRequired, onNewLibraryBackdropClick: PropTypes.func.isRequired, onNewLibraryCostumeClick: PropTypes.func.isRequired, onRequestCloseCameraModal: PropTypes.func.isRequired, + onShowImporting: PropTypes.func.isRequired, sprites: PropTypes.shape({ id: PropTypes.shape({ costumes: PropTypes.arrayOf(PropTypes.shape({ @@ -386,7 +413,9 @@ const mapDispatchToProps = dispatch => ({ }, dispatchUpdateRestore: restoreState => { dispatch(setRestore(restoreState)); - } + }, + onCloseImporting: () => dispatch(closeAlertWithId('importingAsset')), + onShowImporting: () => dispatch(showStandardAlert('importingAsset')) }); export default errorBoundaryHOC('Costume Tab')( diff --git a/src/containers/green-flag-overlay.jsx b/src/containers/green-flag-overlay.jsx index 2d0e4135f..d6dcd44cc 100644 --- a/src/containers/green-flag-overlay.jsx +++ b/src/containers/green-flag-overlay.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import VM from 'scratch-vm'; +import Box from '../components/box/box.jsx'; import greenFlag from '../components/green-flag/icon--green-flag.svg'; class GreenFlagOverlay extends React.Component { @@ -23,15 +24,18 @@ class GreenFlagOverlay extends React.Component { if (this.props.isStarted) return null; return ( -
- -
+
+ +
+
+ ); } } @@ -39,7 +43,8 @@ class GreenFlagOverlay extends React.Component { GreenFlagOverlay.propTypes = { className: PropTypes.string, isStarted: PropTypes.bool, - vm: PropTypes.instanceOf(VM) + vm: PropTypes.instanceOf(VM), + wrapperClass: PropTypes.string }; const mapStateToProps = state => ({ diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 4011ba271..173f1218a 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -60,6 +60,11 @@ class GUI extends React.Component { if (this.props.projectTitle !== prevProps.projectTitle) { this.setReduxTitle(this.props.projectTitle); } + if (this.props.isShowingProject && !prevProps.isShowingProject) { + // this only notifies container when a project changes from not yet loaded to loaded + // At this time the project view in www doesn't need to know when a project is unloaded + this.props.onProjectLoaded(); + } } setReduxTitle (newTitle) { if (newTitle === null || typeof newTitle === 'undefined') { @@ -83,6 +88,7 @@ class GUI extends React.Component { isError, isScratchDesktop, isShowingProject, + onProjectLoaded, onStorageInit, onUpdateProjectId, onUpdateReduxProjectTitle, @@ -113,19 +119,18 @@ GUI.propTypes = { cloudHost: PropTypes.string, error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), fetchingProject: PropTypes.bool, - importInfoVisible: PropTypes.bool, intl: intlShape, isError: PropTypes.bool, isLoading: PropTypes.bool, isScratchDesktop: PropTypes.bool, isShowingProject: PropTypes.bool, loadingStateVisible: PropTypes.bool, + onProjectLoaded: PropTypes.func, onSeeCommunity: PropTypes.func, onStorageInit: PropTypes.func, onUpdateProjectId: PropTypes.func, onUpdateProjectTitle: PropTypes.func, onUpdateReduxProjectTitle: PropTypes.func, - previewInfoVisible: PropTypes.bool, projectHost: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), projectTitle: PropTypes.string, @@ -136,6 +141,7 @@ GUI.propTypes = { GUI.defaultProps = { isScratchDesktop: false, onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(), + onProjectLoaded: () => {}, onUpdateProjectId: () => {} }; @@ -151,13 +157,12 @@ const mapStateToProps = state => { costumeLibraryVisible: state.scratchGui.modals.costumeLibrary, costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX, error: state.scratchGui.projectState.error, - importInfoVisible: state.scratchGui.modals.importInfo, isError: getIsError(loadingState), + isFullScreen: state.scratchGui.mode.isFullScreen, isPlayerOnly: state.scratchGui.mode.isPlayerOnly, isRtl: state.locales.isRtl, isShowingProject: getIsShowingProject(loadingState), loadingStateVisible: state.scratchGui.modals.loadingProject, - previewInfoVisible: state.scratchGui.modals.previewInfo, projectId: state.scratchGui.projectState.projectId, soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX, targetIsStage: ( diff --git a/src/containers/import-modal.jsx b/src/containers/import-modal.jsx deleted file mode 100644 index 1c75929cf..000000000 --- a/src/containers/import-modal.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import bindAll from 'lodash.bindall'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {connect} from 'react-redux'; - -import ImportModalComponent from '../components/import-modal/import-modal.jsx'; - -import { - closeImportInfo, - openPreviewInfo -} from '../reducers/modals'; - -class ImportModal extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleViewProject', - 'handleCancel', - 'handleChange', - 'handleGoBack', - 'handleKeyPress' - ]); - - this.state = { - inputValue: '', - hasValidationError: false, - errorMessage: '' - }; - } - handleKeyPress (event) { - if (event.key === 'Enter') this.handleViewProject(); - } - handleViewProject () { - const inputValue = this.state.inputValue; - const projectId = this.validate(inputValue); - - if (projectId) { - const projectLink = document.createElement('a'); - document.body.appendChild(projectLink); - projectLink.href = `#${projectId}`; - projectLink.click(); - document.body.removeChild(projectLink); - this.props.onViewProject(); - } else { - this.setState({ - hasValidationError: true, - errorMessage: `invalidFormatError`}); - } - } - handleChange (e) { - this.setState({inputValue: e.target.value, hasValidationError: false}); - } - validate (input) { - const urlMatches = input.match(/^(?:https?:\/\/)?scratch\.mit\.edu\/projects\/(\d+)/); - if (urlMatches && urlMatches.length > 0) { - return urlMatches[1]; - } - const projectIdMatches = input.match(/^#?(\d+)$/); - if (projectIdMatches && projectIdMatches.length > 0) { - return projectIdMatches[1]; - } - return null; - } - handleCancel () { - this.props.onCancel(); - } - handleGoBack () { - this.props.onBack(); - } - render () { - return ( - - ); - } -} - -ImportModal.propTypes = { - isRtl: PropTypes.bool, - onBack: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - onViewProject: PropTypes.func -}; - -const mapStateToProps = state => ({ - isRtl: state.locales.isRtl -}); - -const mapDispatchToProps = dispatch => ({ - onBack: () => { - dispatch(closeImportInfo()); - dispatch(openPreviewInfo()); - }, - onCancel: () => { - dispatch(closeImportInfo()); - }, - onViewProject: () => { - dispatch(closeImportInfo()); - } -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(ImportModal); diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx index 9837d5b2a..0f770546d 100644 --- a/src/containers/library-item.jsx +++ b/src/containers/library-item.jsx @@ -1,6 +1,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; +import {injectIntl} from 'react-intl'; import LibraryItemComponent from '../components/library-item/library-item.jsx'; @@ -13,14 +14,21 @@ class LibraryItem extends React.PureComponent { 'handleFocus', 'handleKeyPress', 'handleMouseEnter', - 'handleMouseLeave' + 'handleMouseLeave', + 'rotateIcon', + 'startRotatingIcons', + 'stopRotatingIcons' ]); + this.state = { + iconIndex: 0, + isRotatingIcon: false + }; } - handleBlur () { - this.props.onBlur(this.props.id); + componentWillUnmount () { + clearInterval(this.intervalId); } - handleFocus () { - this.props.onFocus(this.props.id); + handleBlur (id) { + this.handleMouseLeave(id); } handleClick (e) { if (!this.props.disabled) { @@ -28,6 +36,9 @@ class LibraryItem extends React.PureComponent { } e.preventDefault(); } + handleFocus (id) { + this.handleMouseEnter(id); + } handleKeyPress (e) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); @@ -36,47 +47,93 @@ class LibraryItem extends React.PureComponent { } handleMouseEnter () { this.props.onMouseEnter(this.props.id); + if (this.props.icons && this.props.icons.length) { + this.stopRotatingIcons(); + this.setState({ + isRotatingIcon: true + }, this.startRotatingIcons); + } } handleMouseLeave () { this.props.onMouseLeave(this.props.id); + if (this.props.icons && this.props.icons.length) { + this.setState({ + isRotatingIcon: false + }, this.stopRotatingIcons); + } + } + startRotatingIcons () { + this.rotateIcon(); + this.intervalId = setInterval(this.rotateIcon, 300); + } + stopRotatingIcons () { + if (this.intervalId) { + this.intervalId = clearInterval(this.intervalId); + } + } + rotateIcon () { + const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length; + this.setState({iconIndex: nextIconIndex}); + } + curIconSource () { + if (this.props.icons && + this.state.isRotatingIcon && + this.state.iconIndex < this.props.icons.length && + this.props.icons[this.state.iconIndex]) { + return this.props.icons[this.state.iconIndex]; + } + return this.props.iconSource; } render () { - const { - id: _id, - onBlur: _onBlur, - onFocus: _onFocus, - onMouseEnter: _onMouseEnter, - onMouseLeave: _onMouseLeave, - onSelect: _onSelect, - ...childProps - } = this.props; - return (); + const iconSource = this.curIconSource(); + return ( +
+
+ /> +
-
- -
5
- + > +
+ +
+
@@ -49,6 +45,18 @@ exports[`SpriteSelectorItemComponent matches snapshot when given a number and de 480 x 360
+
+ +