diff --git a/.travis.yml b/.travis.yml
index 5805f6b6839..ba21af32461 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -29,15 +29,6 @@ before_deploy:
export BEFORE_DEPLOY_RAN=true
fi
deploy:
-- provider: script
- on:
- all_branches: true
- 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
- script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
- provider: npm
on:
branch:
@@ -59,5 +50,14 @@ deploy:
acl: public_read
skip_cleanup: true
local_dir: build
+- provider: script
+ on:
+ all_branches: true
+ 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
+ script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
after_deploy:
- 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all'
diff --git a/README.md b/README.md
index c036a170930..66378014402 100644
--- a/README.md
+++ b/README.md
@@ -28,33 +28,89 @@ npm start
```
Then go to [http://localhost:8601/](http://localhost:8601/) - the playground outputs the default GUI component
+## Developing alongside other Scratch repositories
+If you wish to develop scratch-gui alongside other scratch repositories that depend on it, you may wish
+to have the other repositories use your local scratch-gui build instead of fetching the current production
+version of the scratch-gui that is found by default using `npm install`.
+
+To do this:
+1. Make sure you have run `npm install` from this (scratch-gui) repository's top level
+2. Make sure you have run `npm install` from the top level of each repository (such as scratch-www) that depends on scratch-gui
+3. From this (scratch-gui) repository's top level, build the `dist` directory by running `BUILD_MODE=dist npm run build`
+4. From this (scratch-gui) repository's top level, establish a link to this repository by running `npm link`
+5. From the top level of each repository that depends on scratch-gui, run `npm link scratch-gui`
+6. Build or run the repositories that depend on scratch-gui
+
+Instead of `BUILD_MODE=dist npm run build` you can also use `BUILD_MODE=dist npm run watch`, however this may be unreliable.
+
+### Oh no! It didn't work!
+* Follow the recipe above step by step and don't change the order. It is especially important to run npm first because installing after the linking will reset the linking.
+* Make sure the repositories are siblings on your machine's file tree.
+* If you have multiple Terminal tabs or windows open for the different Scratch repositories, make sure to use the same node version in all of them.
+* In the worst case unlink the repositories by running `npm unlink` in both, and start over.
+
## Testing
-NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.
+### Documentation
+
+You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests.
+
+See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options.
-Run linter, unit tests, build, and integration tests.
+### Running tests
+
+*NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.*
+
+Before running any test, make sure you have run `npm install` from this (scratch-gui) repository's top level.
+
+#### Main testing command
+
+To run linter, unit tests, build, and integration tests, all at once:
```bash
npm test
```
-Run unit tests in isolation.
+#### Running unit tests
+
+To run unit tests in isolation:
+```bash
+npm run test:unit
+```
+
+To run unit tests in watch mode (watches for code changes and continuously runs tests):
```bash
-npm run unit-test
+npm run test:unit -- --watch
```
-Run unit tests in watch mode (watches for code changes and continuously runs tests). See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options.
+#### Running integration tests
+
+Integration tests use a headless browser to manipulate the actual html and javascript that the repo
+produces. You will not see this activity (though you can hear it when sounds are played!).
+
+Note that integration tests require you to first create a build that can be loaded in a browser:
+
```bash
-npm run unit-test -- --watch
+npm run build
```
-Run integration tests in isolation.
+Then, you can run all integration tests:
+
```bash
-npm run integration-test
+npm run test:integration
```
-You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests.
+Or, you can run a single file of integration tests (in this example, the `backpack` tests):
-## Publishing to GitHub Pages
+```bash
+$(npm bin)/jest --runInBand test/integration/backpack.test.js
+```
+If you want to watch the browser as it runs the test, rather than running headless, use:
+
+```bash
+USE_HEADLESS=no $(npm bin)/jest --runInBand test/integration/backpack.test.js
+```
+
+## Publishing to GitHub Pages
You can publish the GUI to github.io so that others on the Internet can view it.
[Read the wiki for a step-by-step guide.](https://github.com/LLK/scratch-gui/wiki/Publishing-to-GitHub-Pages)
diff --git a/package.json b/package.json
index 1b589d22840..620455b8ea7 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,8 @@
"react-dom": "^16.0.0"
},
"devDependencies": {
- "arraybuffer-loader": "^1.0.3",
- "autoprefixer": "^8.1.0",
+ "arraybuffer-loader": "^1.0.6",
+ "autoprefixer": "^9.0.1",
"babel-core": "^6.23.1",
"babel-eslint": "^8.0.1",
"babel-loader": "^7.1.0",
@@ -40,14 +40,14 @@
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.22.0",
"base64-loader": "1.0.0",
- "bowser": "1.9.3",
- "chromedriver": "2.40.0",
+ "bowser": "1.9.4",
+ "chromedriver": "2.42.0",
"classnames": "2.2.6",
"copy-webpack-plugin": "^4.5.1",
"core-js": "2.5.7",
- "css-loader": "^0.28.11",
- "enzyme": "^3.1.0",
- "enzyme-adapter-react-16": "1.1.1",
+ "css-loader": "^1.0.0",
+ "enzyme": "^3.5.0",
+ "enzyme-adapter-react-16": "1.3.0",
"es6-object-assign": "1.1.0",
"eslint": "^5.0.1",
"eslint-config-scratch": "^5.0.0",
@@ -70,50 +70,50 @@
"lodash.pick": "4.4.0",
"minilog": "3.1.0",
"mkdirp": "^0.5.1",
- "postcss-import": "^11.0.0",
- "postcss-loader": "^2.1.4",
- "postcss-simple-vars": "^4.0.0",
+ "postcss-import": "^12.0.0",
+ "postcss-loader": "^3.0.0",
+ "postcss-simple-vars": "^5.0.1",
"prop-types": "^15.5.10",
"raf": "^3.4.0",
"raw-loader": "^0.5.1",
"react": "16.2.0",
- "react-contextmenu": "2.9.2",
+ "react-contextmenu": "2.9.3",
"react-dom": "16.2.0",
"react-draggable": "3.0.5",
"react-ga": "2.5.3",
"react-intl": "2.4.0",
- "react-modal": "3.4.4",
- "react-popover": "0.5.7",
+ "react-modal": "3.6.1",
+ "react-popover": "0.5.10",
"react-redux": "5.0.7",
- "react-responsive": "4.1.0",
- "react-style-proptype": "3.2.1",
- "react-tabs": "2.2.2",
+ "react-responsive": "5.0.0",
+ "react-style-proptype": "3.2.2",
+ "react-tabs": "2.3.0",
"react-test-renderer": "16.2.0",
- "react-tooltip": "3.6.1",
- "react-virtualized": "9.20.0",
+ "react-tooltip": "3.8.0",
+ "react-virtualized": "9.20.1",
"redux": "3.7.2",
"redux-mock-store": "^1.2.3",
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20180625202813",
- "scratch-blocks": "0.1.0-prerelease.1531482946",
- "scratch-l10n": "3.0.20180712200642",
- "scratch-paint": "0.2.0-prerelease.20180712195436",
- "scratch-render": "0.1.0-prerelease.20180618173030",
- "scratch-storage": "0.5.1",
- "scratch-svg-renderer": "0.2.0-prerelease.20180712223402",
- "scratch-vm": "0.1.0-prerelease.1531486395",
+ "scratch-blocks": "0.1.0-prerelease.1538658492",
+ "scratch-l10n": "3.0.20181004141631",
+ "scratch-paint": "0.2.0-prerelease.20180926191006",
+ "scratch-render": "0.1.0-prerelease.20181002192350",
+ "scratch-storage": "1.0.3",
+ "scratch-svg-renderer": "0.2.0-prerelease.20180926143036",
+ "scratch-vm": "0.2.0-prerelease.20181001223744",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
- "style-loader": "^0.21.0",
+ "style-loader": "^0.23.0",
"svg-to-image": "1.1.3",
- "text-encoding": "0.6.4",
+ "text-encoding": "0.7.0",
"to-style": "1.3.3",
"uglifyjs-webpack-plugin": "^1.2.5",
"wav-encoder": "1.3.0",
"web-audio-test-api": "^0.5.2",
"webpack": "^4.6.0",
- "webpack-cli": "^2.0.15",
+ "webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.3",
"xhr": "2.5.0"
},
diff --git a/src/.eslintrc.js b/src/.eslintrc.js
index 14b082cdfe7..4b05b2d2747 100644
--- a/src/.eslintrc.js
+++ b/src/.eslintrc.js
@@ -12,7 +12,10 @@ module.exports = {
'import/no-commonjs': 'error',
'import/no-amd': 'error',
'import/no-nodejs-modules': 'error',
- 'react/jsx-no-literals': 'error'
+ 'react/jsx-no-literals': 'error',
+ 'no-confusing-arrow': ['error', {
+ 'allowParens': true
+ }]
},
settings: {
react: {
diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css
index 8f39946c128..e82103ca10a 100644
--- a/src/components/action-menu/action-menu.css
+++ b/src/components/action-menu/action-menu.css
@@ -1,4 +1,5 @@
@import "../../css/colors.css";
+@import "../../css/z-index.css";
$main-button-size: 2.75rem;
$more-button-size: 2.25rem;
@@ -44,7 +45,7 @@ button::-moz-focus-inner {
width: $main-button-size;
height: $main-button-size;
box-shadow: 0 0 0 4px $motion-transparent;
- z-index: 20; /* TODO reorder layout to prevent z-index need */
+ z-index: $z-index-add-button;
transition: transform, box-shadow 0.5s;
}
@@ -58,6 +59,10 @@ button::-moz-focus-inner {
height: calc($main-button-size - 1rem);
}
+[dir="rtl"] .main-icon {
+ transform: scaleX(-1);
+}
+
.more-buttons-outer {
/*
Need to use two divs to set different overflow x/y
@@ -71,6 +76,7 @@ button::-moz-focus-inner {
border-top-right-radius: $more-button-size;
width: $more-button-size;
margin-left: calc(($main-button-size - $more-button-size) / 2);
+ margin-right: calc(($main-button-size - $more-button-size) / 2);
position: absolute;
bottom: calc($main-button-size);
@@ -150,7 +156,7 @@ button::-moz-focus-inner {
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;
+ z-index: $z-index-tooltip !important;
}
$arrow-size: 0.5rem;
diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx
index 2725c502b42..01070b27f49 100644
--- a/src/components/action-menu/action-menu.jsx
+++ b/src/components/action-menu/action-menu.jsx
@@ -24,6 +24,7 @@ class ActionMenu extends React.Component {
isOpen: false,
forceHide: false
};
+ this.mainTooltipId = `tooltip-${Math.random()}`;
}
componentDidMount () {
// Touch start on the main button is caught to trigger open and not click
@@ -101,11 +102,10 @@ class ActionMenu extends React.Component {
img: mainImg,
title: mainTitle,
moreButtons,
+ tooltipPlace,
onClick
} = this.props;
- const mainTooltipId = `tooltip-${Math.random()}`;
-
return (
@@ -142,7 +142,7 @@ class ActionMenu extends React.Component {
fileAccept, fileChange, fileInput}, keyId) => {
const isComingSoon = !handleClick;
const hasFileInput = fileInput;
- const tooltipId = title;
+ const tooltipId = `${this.mainTooltipId}-${title}`;
return (
);
@@ -198,7 +198,8 @@ ActionMenu.propTypes = {
fileInput: PropTypes.func // Optional, only for file upload
})),
onClick: PropTypes.func.isRequired,
- title: PropTypes.node.isRequired
+ title: PropTypes.node.isRequired,
+ tooltipPlace: PropTypes.string
};
export default ActionMenu;
diff --git a/src/components/alerts/alert.css b/src/components/alerts/alert.css
new file mode 100644
index 00000000000..c4f00a74191
--- /dev/null
+++ b/src/components/alerts/alert.css
@@ -0,0 +1,36 @@
+@import "../../css/units.css";
+@import "../../css/colors.css";
+@import "../../css/z-index.css";
+
+.alert {
+ width: 100%;
+ background: #FFF0DF;
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
+ align-items: left;
+ border: 1px solid #FF8C1A;
+ border-radius: 8px;
+ padding: 8px;
+ box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25);
+ margin-bottom: 7px;
+}
+
+.alert-icon {
+ margin-right: 5px;
+ vertical-align: middle;
+}
+
+.alert-message {
+ color: #555;
+ font-weight: bold;
+ font-size: 12px;
+ line-height: 22pt;
+ width: 100%;
+}
+
+.alert-close-button {
+ margin-top: 7px;
+ margin-right: 4px;
+ outline-style:none;
+}
diff --git a/src/components/alerts/alert.jsx b/src/components/alerts/alert.jsx
new file mode 100644
index 00000000000..d0f65309e08
--- /dev/null
+++ b/src/components/alerts/alert.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Box from '../box/box.jsx';
+import CloseButton from '../close-button/close-button.jsx';
+
+import styles from './alert.css';
+
+const AlertComponent = ({
+ iconURL,
+ message,
+ onCloseAlert
+}) => (
+
+
+ {iconURL ? (
+
+ ) : null}
+ {message}
+
+
+
+);
+
+AlertComponent.propTypes = {
+ iconURL: PropTypes.string,
+ message: PropTypes.string,
+ onCloseAlert: PropTypes.func.isRequired
+};
+
+export default AlertComponent;
diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx
new file mode 100644
index 00000000000..1835a50a820
--- /dev/null
+++ b/src/components/alerts/alerts.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Box from '../box/box.jsx';
+import Alert from '../../containers/alert.jsx';
+
+const AlertsComponent = ({
+ alertsList,
+ className,
+ onCloseAlert
+}) => (
+
+ {alertsList.map((a, index) => (
+
+ ))}
+
+);
+
+AlertsComponent.propTypes = {
+ alertsList: PropTypes.arrayOf(PropTypes.object),
+ className: PropTypes.string,
+ onCloseAlert: PropTypes.func
+};
+
+export default AlertsComponent;
diff --git a/src/components/asset-panel/asset-panel.css b/src/components/asset-panel/asset-panel.css
index 9a999057b57..bfd578909fd 100644
--- a/src/components/asset-panel/asset-panel.css
+++ b/src/components/asset-panel/asset-panel.css
@@ -5,16 +5,32 @@
display: flex;
flex-grow: 1;
border: 1px solid $ui-black-transparent;
- border-top-right-radius: $space;
background: white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 0.85rem;
}
+[dir="ltr"] .wrapper {
+ border-top-right-radius: $space;
+ border-bottom-right-radius: $space;
+}
+
+[dir="rtl"] .wrapper {
+ border-top-left-radius: $space;
+ border-bottom-left-radius: $space;
+}
+
.detail-area {
display: flex;
flex-grow: 1;
flex-shrink: 1;
- border-left: 1px solid $ui-black-transparent;
overflow-y: auto;
}
+
+[dir="ltr"] .detail-area {
+ border-left: 1px solid $ui-black-transparent;
+}
+
+[dir="rtl"] .detail-area {
+ border-right: 1px solid $ui-black-transparent;
+}
diff --git a/src/components/asset-panel/icon--sound-rtl.svg b/src/components/asset-panel/icon--sound-rtl.svg
new file mode 100644
index 00000000000..adf85017394
--- /dev/null
+++ b/src/components/asset-panel/icon--sound-rtl.svg
@@ -0,0 +1,20 @@
+
+
+
+ Artboard
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css
index 3dc4e803d9e..535c38b0be6 100644
--- a/src/components/asset-panel/selector.css
+++ b/src/components/asset-panel/selector.css
@@ -32,6 +32,7 @@ $fade-out-distance: 100px;
position: absolute;
bottom: 0;
left: 0;
+ right:0;
background: linear-gradient(rgba(232,237,241, 0),rgba(232,237,241, 1));
height: $fade-out-distance;
width: 100%;
@@ -49,6 +50,8 @@ $fade-out-distance: 100px;
overflow-y: scroll;
display: flex;
flex-direction: column;
+ /* Make sure there is room to scroll beyond the last tile */
+ padding-bottom: 70px;
}
.list-item {
@@ -70,5 +73,5 @@ $fade-out-distance: 100px;
.list-item.placeholder {
background: black;
- filter: opacity(15%) brightness(20%);
+ filter: opacity(15%) brightness(0%);
}
diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css
index 9143844d097..33585ca90de 100644
--- a/src/components/backpack/backpack.css
+++ b/src/components/backpack/backpack.css
@@ -9,7 +9,6 @@
.backpack-header {
margin-top: 0.5rem;
border: 1px solid $ui-black-transparent;
- border-top-right-radius: $space;
background: $ui-white;
padding: 0.25rem;
text-align: center;
@@ -20,6 +19,14 @@
user-select: none;
}
+[dir="ltr"] .backpack-header {
+ border-top-right-radius: $space;
+}
+
+[dir="rtl"] .backpack-header {
+ border-top-left-radius: $space;
+}
+
.backpack-list {
position: relative;
display: flex;
diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx
index 0f7b0b89f7e..04cdc293798 100644
--- a/src/components/backpack/backpack.jsx
+++ b/src/components/backpack/backpack.jsx
@@ -17,7 +17,7 @@ const dragTypeMap = {
sprite: DragConstants.BACKPACK_SPRITE
};
-const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle, onDelete}) => (
+const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, onToggle, onDelete}) => (
{error ? (
@@ -104,6 +104,7 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on
);
Backpack.propTypes = {
+ containerRef: PropTypes.func,
contents: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
thumbnailUrl: PropTypes.string,
@@ -111,7 +112,6 @@ Backpack.propTypes = {
name: PropTypes.string
})),
dragOver: PropTypes.bool,
- dropAreaRef: PropTypes.func,
error: PropTypes.bool,
expanded: PropTypes.bool,
loading: PropTypes.bool,
diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css
index e02486ce0fe..3d87405893c 100644
--- a/src/components/blocks/blocks.css
+++ b/src/components/blocks/blocks.css
@@ -12,6 +12,13 @@
border-bottom-right-radius: $space;
}
+[dir="rtl"] .blocks :global(.injectionDiv) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-top-left-radius: $space;
+ border-bottom-left-radius: $space;
+}
+
.blocks :global(.blocklyMainBackground) {
stroke: none;
}
@@ -31,6 +38,11 @@
-ms-overflow-style: none;
}
+[dir="rtl"] .blocks :global(.blocklyToolboxDiv) {
+ border-right: none;
+ border-left: 1px solid $ui-black-transparent;
+}
+
.blocks :global(.blocklyToolboxDiv::-webkit-scrollbar) {
display: none;
}
@@ -40,6 +52,12 @@
box-sizing: content-box;
}
+[dir="rtl"] .blocks :global(.blocklyFlyout) {
+ border-right: none;
+ border-left: 1px solid $ui-black-transparent;
+}
+
+
.blocks :global(.blocklyBlockDragSurface) {
/*
Fix an issue where the drag surface was preventing hover events for sharing blocks.
diff --git a/src/components/browser-modal/browser-modal.css b/src/components/browser-modal/browser-modal.css
index aa8840c4cc8..a7f86ac4efe 100644
--- a/src/components/browser-modal/browser-modal.css
+++ b/src/components/browser-modal/browser-modal.css
@@ -34,6 +34,10 @@
background-size: cover;
}
+[dir="rtl"] .illustration {
+ transform: scaleX(-1);
+}
+
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx
index bb5c82fcffa..9afac0e3891 100644
--- a/src/components/browser-modal/browser-modal.jsx
+++ b/src/components/browser-modal/browser-modal.jsx
@@ -22,63 +22,70 @@ const BrowserModal = ({intl, ...props}) => (
overlayClassName={styles.modalOverlay}
onRequestClose={props.onBack}
>
-
+
+
-
-
-
-
-
- { /* eslint-disable max-len */ }
-
- { /* eslint-enable max-len */ }
-
-
-
-
+
+
+
+
+
+ { /* eslint-disable max-len */ }
-
+ { /* eslint-enable max-len */ }
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
-
-
-
-
- )
- }}
- />
-
-
+
);
BrowserModal.propTypes = {
intl: intlShape.isRequired,
+ isRtl: PropTypes.bool,
onBack: PropTypes.func.isRequired
};
-export default injectIntl(BrowserModal);
+const WrappedBrowserModal = injectIntl(BrowserModal);
+
+WrappedBrowserModal.setAppElement = ReactModal.setAppElement;
+
+export default WrappedBrowserModal;
diff --git a/src/components/button/button.css b/src/components/button/button.css
index 9ce746c5214..9ba2fbd93b4 100644
--- a/src/components/button/button.css
+++ b/src/components/button/button.css
@@ -11,10 +11,17 @@
}
.icon {
- margin-right: .5rem;
height: 1.5rem;
}
+[dir="ltr"] .icon {
+ margin-right: .5rem;
+}
+
+[dir="rtl"] .icon {
+ margin-left: .5rem;
+}
+
.content {
white-space: nowrap;
}
diff --git a/src/components/camera-modal/camera-modal.css b/src/components/camera-modal/camera-modal.css
index 4ea962fbafd..7522fbb2ec2 100644
--- a/src/components/camera-modal/camera-modal.css
+++ b/src/components/camera-modal/camera-modal.css
@@ -135,6 +135,10 @@ $main-button-size: 2.75rem;
color: $ui-white;
}
+[dir="rtl"] .retake-button img {
+ transform: scaleX(-1);
+}
+
@keyframes flash {
0% { opacity: 1; }
100% { opacity: 0; }
diff --git a/src/components/camera-modal/camera-modal.jsx b/src/components/camera-modal/camera-modal.jsx
index f115a6f8a75..8c499c36249 100644
--- a/src/components/camera-modal/camera-modal.jsx
+++ b/src/components/camera-modal/camera-modal.jsx
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import Box from '../box/box.jsx';
-import Modal from '../modal/modal.jsx';
+import Modal from '../../containers/modal.jsx';
import styles from './camera-modal.css';
import backIcon from './icon--back.svg';
import cameraIcon from '../action-menu/icon--camera.svg';
@@ -79,7 +79,7 @@ const CameraModal = ({intl, ...props}) => (
{props.capture ?
diff --git a/src/components/cards/card.css b/src/components/cards/card.css
index 9e14a1fb2a6..439c8191f9d 100644
--- a/src/components/cards/card.css
+++ b/src/components/cards/card.css
@@ -153,7 +153,7 @@
display: flex;
flex-direction: row;
justify-content: space-around;
- padding: 0 0.5rem 0.5rem;
+ padding: 0 1rem 0.5rem;
}
.deck {
@@ -167,7 +167,7 @@
}
.deck-image {
- width: 130px;
+ width: 200px;
height: 100px;
object-fit: cover;
}
@@ -180,21 +180,33 @@
text-align: center;
font-weight: bold;
text-align: center;
+ max-width: 200px;
}
.help-icon, .close-icon {
height: 1rem;
}
-.help-icon {
+[dir="ltr"] .help-icon {
margin-right: 0.25rem;
}
-.close-icon {
+[dir="rtl"] .help-icon {
margin-left: 0.25rem;
+}
+
+.close-icon {
transform: rotate(45deg);
}
+[dir="ltr"] .close-icon {
+ margin-left: 0.25rem;
+}
+
+[dir="rtl"] .close-icon {
+ margin-right: 0.25rem;
+}
+
.see-all {
display: flex;
flex-direction: row;
@@ -219,10 +231,14 @@
text-align: center;
}
-.see-all-button img {
+[dir="ltr"] .see-all-button img {
margin-left: 0.5rem;
}
+[dir="rtl"] .see-all-button img {
+ margin-right: 0.5rem;
+}
+
.video-cover {
width: 100%;
height: 100%;
diff --git a/src/components/cards/cards.jsx b/src/components/cards/cards.jsx
index dc081ae0b64..24e3d6edd94 100644
--- a/src/components/cards/cards.jsx
+++ b/src/components/cards/cards.jsx
@@ -5,8 +5,8 @@ import Draggable from 'react-draggable';
import styles from './card.css';
-import nextIcon from './icon--next.svg';
-import prevIcon from './icon--prev.svg';
+import rightArrow from './icon--next.svg';
+import leftArrow from './icon--prev.svg';
import helpIcon from '../../lib/assets/icon--tutorials.svg';
import closeIcon from '../close-button/icon--close.svg';
@@ -43,9 +43,9 @@ const CardHeader = ({onCloseCards, onShowAll, totalSteps, step}) => (
onClick={onCloseCards}
>
(
+const NextPrevButtons = ({isRtl, onNextStep, onPrevStep}) => (
{onNextStep ? (
) : null}
{onPrevStep ? (
@@ -132,6 +132,7 @@ const NextPrevButtons = ({onNextStep, onPrevStep}) => (
);
NextPrevButtons.propTypes = {
+ isRtl: PropTypes.bool,
onNextStep: PropTypes.func,
onPrevStep: PropTypes.func
};
@@ -152,7 +153,7 @@ const PreviewsStep = ({deckIds, content, onActivateDeckFactory, onShowAll}) => (
/>
- {deckIds.map(id => (
+ {deckIds.slice(0, 2).map(id => (
{
- if (props.activeDeckId === null) return;
+ const {
+ activeDeckId,
+ content,
+ dragging,
+ isRtl,
+ onActivateDeckFactory,
+ onCloseCards,
+ onDrag,
+ onStartDrag,
+ onEndDrag,
+ onShowAll,
+ onNextStep,
+ onPrevStep,
+ step,
+ ...posProps
+ } = props;
+ let {x, y} = posProps;
- const steps = props.content[props.activeDeckId].steps;
+ if (activeDeckId === null) return;
+
+ if (x === 0 && y === 0) {
+ // initialize positions
+ x = isRtl ? -292 : 292;
+ y = 365;
+ }
+
+ const steps = content[activeDeckId].steps;
return (
- {steps[props.step].deckIds ? (
+ {steps[step].deckIds ? (
) : (
- steps[props.step].video ? (
+ steps[step].video ? (
) : (
)
)}
0 ? props.onPrevStep : null}
+ isRtl={isRtl}
+ onNextStep={step < steps.length - 1 ? onNextStep : null}
+ onPrevStep={step > 0 ? onPrevStep : null}
/>
@@ -268,6 +294,7 @@ Cards.propTypes = {
})
}),
dragging: PropTypes.bool.isRequired,
+ isRtl: PropTypes.bool,
onActivateDeckFactory: PropTypes.func.isRequired,
onCloseCards: PropTypes.func.isRequired,
onDrag: PropTypes.func,
diff --git a/src/components/close-button/close-button.css b/src/components/close-button/close-button.css
index 072d5abff1a..90bd473ce93 100644
--- a/src/components/close-button/close-button.css
+++ b/src/components/close-button/close-button.css
@@ -20,6 +20,11 @@
box-shadow: 0 0 0 4px $ui-black-transparent;
}
+.close-button.large.orange:hover {
+ transform: scale(1.1, 1.1);
+ box-shadow: 0px 0px 0px 4px hsla(29, 100%, 54%, 0.2);
+}
+
.small {
width: 0.825rem;
height: 0.825rem;
@@ -33,6 +38,11 @@
box-shadow: 0 0 0 2px $ui-black-transparent;
}
+.large.orange {
+ background-color: hsla(29, 100%, 54%, 0.2);
+ box-shadow: 0px 0px 0px 2px hsla(29, 100%, 54%, 0.2);
+}
+
.close-icon {
position: relative;
margin: 0.25rem;
@@ -41,6 +51,11 @@
transform: rotate(45deg);
}
+.close-icon.orange {
+ transform: rotate(45deg);
+ transform: scale(1.4);
+}
+
.small .close-icon {
width: 50%;
}
diff --git a/src/components/close-button/close-button.jsx b/src/components/close-button/close-button.jsx
index 98cf78d0260..b41a3d97c4f 100644
--- a/src/components/close-button/close-button.jsx
+++ b/src/components/close-button/close-button.jsx
@@ -4,6 +4,7 @@ import classNames from 'classnames';
import styles from './close-button.css';
import closeIcon from './icon--close.svg';
+import closeIconOrange from './icon--close-orange.svg';
import backIcon from '../../lib/assets/icon--back.svg';
const CloseButton = props => (
@@ -14,7 +15,8 @@ const CloseButton = props => (
props.className,
{
[styles.small]: props.size === CloseButton.SIZE_SMALL,
- [styles.large]: props.size === CloseButton.SIZE_LARGE
+ [styles.large]: props.size === CloseButton.SIZE_LARGE,
+ [styles.orange]: props.color === CloseButton.COLOR_ORANGE
}
)}
role="button"
@@ -27,8 +29,16 @@ const CloseButton = props => (
src={backIcon}
/> :
}
@@ -36,10 +46,12 @@ const CloseButton = props => (
CloseButton.SIZE_SMALL = 'small';
CloseButton.SIZE_LARGE = 'large';
+CloseButton.COLOR_ORANGE = 'orange';
CloseButton.propTypes = {
buttonType: PropTypes.oneOf(['back', 'close']),
className: PropTypes.string,
+ color: PropTypes.string,
onClick: PropTypes.func.isRequired,
size: PropTypes.oneOf([CloseButton.SIZE_SMALL, CloseButton.SIZE_LARGE])
};
diff --git a/src/components/close-button/icon--close-orange.svg b/src/components/close-button/icon--close-orange.svg
new file mode 100644
index 00000000000..7bd66335514
--- /dev/null
+++ b/src/components/close-button/icon--close-orange.svg
@@ -0,0 +1,20 @@
+
+
+
+ Icon
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/coming-soon/coming-soon.css b/src/components/coming-soon/coming-soon.css
index bdd7f87eb4f..900173e420d 100644
--- a/src/components/coming-soon/coming-soon.css
+++ b/src/components/coming-soon/coming-soon.css
@@ -16,7 +16,7 @@
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
font-size: 1rem !important;
line-height: 1.25rem !important;
- z-index: $z-index-coming-soon !important;
+ z-index: $z-index-tooltip !important;
}
.coming-soon:after {
@@ -62,8 +62,15 @@
}
.coming-soon-image {
- margin-left: .125rem;
width: 1.25rem;
height: 1.25rem;
vertical-align: middle;
}
+
+[dir="ltr"] .coming-soon-image {
+ margin-left: .125rem;
+}
+
+[dir="rtl"] .coming-soon-image {
+ margin-right: .125rem;
+}
diff --git a/src/components/connection-modal/auto-scanning-step.jsx b/src/components/connection-modal/auto-scanning-step.jsx
new file mode 100644
index 00000000000..a73c0b58db9
--- /dev/null
+++ b/src/components/connection-modal/auto-scanning-step.jsx
@@ -0,0 +1,155 @@
+import {FormattedMessage} from 'react-intl';
+import PropTypes from 'prop-types';
+import React from 'react';
+import keyMirror from 'keymirror';
+import classNames from 'classnames';
+
+import Box from '../box/box.jsx';
+import Dots from './dots.jsx';
+
+import closeIcon from '../close-button/icon--close.svg';
+
+import radarIcon from './icons/searching.png';
+import bluetoothIcon from './icons/bluetooth-white.svg';
+import backIcon from './icons/back.svg';
+
+import styles from './connection-modal.css';
+
+const PHASES = keyMirror({
+ prescan: null,
+ pressbutton: null,
+ notfound: null
+});
+
+const AutoScanningStep = props => (
+
+
+
+
+ {props.phase === PHASES.prescan && (
+
+
+
+
+ )}
+ {props.phase === PHASES.pressbutton && (
+
+
+
+
+ )}
+ {props.phase === PHASES.notfound && (
+
+
+
+ )}
+
+
+
+
+
+ {props.phase === PHASES.prescan && (
+
+ )}
+ {props.phase === PHASES.pressbutton && (
+
+ )}
+
+
+ {props.phase === PHASES.prescan && (
+
+
+
+ )}
+ {props.phase === PHASES.pressbutton && (
+
+
+
+
+
+
+
+
+ )}
+ {props.phase === PHASES.notfound && (
+
+
+
+
+ )}
+
+
+);
+
+AutoScanningStep.propTypes = {
+ onRefresh: PropTypes.func,
+ onStartScan: PropTypes.func,
+ peripheralButtonImage: PropTypes.string,
+ phase: PropTypes.oneOf(Object.keys(PHASES))
+};
+
+AutoScanningStep.defaultProps = {
+ phase: PHASES.prescan
+};
+
+export {
+ AutoScanningStep as default,
+ PHASES
+};
diff --git a/src/components/connection-modal/connected-step.jsx b/src/components/connection-modal/connected-step.jsx
index c02f644bc4e..aa71c63c8a1 100644
--- a/src/components/connection-modal/connected-step.jsx
+++ b/src/components/connection-modal/connected-step.jsx
@@ -12,10 +12,10 @@ const ConnectedStep = props => (
-
+
(
);
ConnectedStep.propTypes = {
- deviceImage: PropTypes.string.isRequired,
onCancel: PropTypes.func,
- onDisconnect: PropTypes.func
+ onDisconnect: PropTypes.func,
+ peripheralImage: PropTypes.string.isRequired
};
export default ConnectedStep;
diff --git a/src/components/connection-modal/connecting-step.jsx b/src/components/connection-modal/connecting-step.jsx
index 063e67a826c..e4753628677 100644
--- a/src/components/connection-modal/connecting-step.jsx
+++ b/src/components/connection-modal/connecting-step.jsx
@@ -14,10 +14,10 @@ const ConnectingStep = props => (
-
+
(
-
+ {props.connectingMessage}
(
);
ConnectingStep.propTypes = {
- deviceImage: PropTypes.string.isRequired,
- onDisconnect: PropTypes.func
+ connectingMessage: PropTypes.node.isRequired,
+ onDisconnect: PropTypes.func,
+ peripheralImage: PropTypes.string.isRequired
};
export default ConnectingStep;
diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css
index b636b4af9cd..f024489c591 100644
--- a/src/components/connection-modal/connection-modal.css
+++ b/src/components/connection-modal/connection-modal.css
@@ -25,14 +25,14 @@
align-items: center;
}
-.device-tile-pane {
+.peripheral-tile-pane {
overflow-y: auto;
width: 100%;
height: 100%;
padding: 0.5rem;
}
-.device-tile {
+.peripheral-tile {
display: flex;
flex-direction: row;
justify-content: space-between;
@@ -46,32 +46,36 @@
margin-bottom: 0.5rem;
}
-.device-tile-name {
+.peripheral-tile-name {
display: flex;
align-items: center;
}
-.device-tile-image {
+[dir="ltr"] .peripheral-tile-image {
margin-right: 0.5rem;
}
-.device-tile-name-wrapper {
+[dir="rtl"] .peripheral-tile-image {
+ margin-left: 0.5rem;
+}
+
+.peripheral-tile-name-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
-.device-tile-name-label {
+.peripheral-tile-name-label {
font-weight: bold;
font-size: 0.625rem;
}
-.device-tile-name-text {
+.peripheral-tile-name-text {
font-size: 0.875rem;
}
-.device-tile button {
+.peripheral-tile button {
padding: 0.6rem 0.75rem;
border: none;
border-radius: 0.25rem;
@@ -89,9 +93,16 @@
align-items: flex-end;
width: 22px;
height: 16px;
+}
+
+[dir="ltr"] .signal-strength-meter {
margin-right: 1rem;
}
+[dir="rtl"] .signal-strength-meter {
+ margin-left: 1rem;
+}
+
.signal-bar {
width: 4px;
border-radius: 4px;
@@ -107,33 +118,60 @@
background-color: $pen-primary;
}
-.radar {
+.radar-small {
width: 40px;
height: 40px;
+}
+
+[dir="ltr"] .radar-small {
margin-right: 0.5rem;
+}
+
+[dir="rtl"] .radar-small {
+ margin-left: 0.5rem;
+}
+
+.radar-big {
+ width: 120px;
+ height: 120px;
+}
+
+.radar-spin {
animation: spin 4s linear infinite;
}
+[dir="ltr"] .radar {
+ margin-right: .5rem;
+}
+
+[dir="rtl"] .radar {
+ margin-left: .5rem;
+}
+
@keyframes spin {
100% {
transform: rotate(360deg);
}
}
-
-.device-activity {
+.peripheral-activity {
position: relative;
}
-.device-activity-icon {
+.peripheral-activity-icon {
/* width: 80px;
height: 80px; */
}
+.peripheral-button-image {
+ position: absolute;
+}
+
.bluetooth-connecting-icon {
position: absolute;
top: -5px;
right: -15px;
+ left: -15px;
padding: 5px 5px;
background-color: $motion-primary;
border-radius: 100%;
@@ -149,19 +187,17 @@
}
}
-
.bluetooth-connected-icon {
position: absolute;
top: -5px;
right: -15px;
+ left: -15px;
padding: 5px 5px;
background-color: $pen-primary;
border-radius: 100%;
box-shadow: 0px 0px 0px 4px $pen-transparent;
}
-
-
@keyframes wiggle {
0% {transform: rotate(3deg) scale(1.05);}
25% {transform: rotate(-3deg) scale(1.05);}
@@ -170,7 +206,15 @@
100% {transform: rotate(0deg) scale(1.05);}
}
-.device-tile-widgets {
+.bluetooth-centered-icon {
+ position: absolute;
+ padding: 5px 5px;
+ background-color: $motion-primary;
+ border-radius: 100%;
+ box-shadow: 0px 0px 0px 2px $motion-transparent;
+}
+
+.peripheral-tile-widgets {
display: flex;
align-items: center;
}
@@ -183,8 +227,59 @@
align-items: center;
}
+.scratch-link-help {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ height: 100%;
+ padding: 1rem 0;
+}
+
+.scratch-link-help-step {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+}
+
+[dir="ltr"] .scratch-link-help-step {
+ margin-left: 3rem;
+}
+
+[dir="rtl"] .scratch-link-help-step {
+ margin-right: 3rem;
+}
+
.scratch-link-icon {
- max-width: 150px;
+ max-width: 50px;
+}
+
+[dir="ltr"] .help-step-image {
+ margin-right: 0.5rem;
+}
+
+[dir="rtl"] .help-step-image {
+ margin-left: 0.5rem;
+}
+
+.help-step-number {
+ background: $pen-primary;
+ border-radius: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: $ui-white;
+ font-weight: bold;
+ min-width: 2rem;
+ height: 2rem;
+}
+
+[dir="ltr"] .help-step-number {
+ margin-right: 0.5rem;
+}
+
+[dir="rtl"] .help-step-number {
+ margin-left: 0.5rem;
}
.button-row {
@@ -232,14 +327,26 @@
border-bottom-left-radius: 0;
}
-.button-icon-right {
+[dir="ltr"] .button-icon-right {
margin-left: 0.5rem;
}
+[dir="rtl"] .button-icon-right {
+ margin-right: 0.5rem;
+}
-.button-icon-left {
+[dir="ltr"] .button-icon-left {
margin-right: 0.5rem;
}
+[dir="rtl"] .button-icon-left {
+ margin-left: 0.5rem;
+}
+
+/* reverse back arrow icon for RTL, don't reverse other connection icons */
+[dir="rtl"] .button-icon-back {
+ transform: scaleX(-1);
+}
+
.red-button {
background: $red-primary;
}
@@ -262,7 +369,7 @@
.instructions {
text-align: center;
- padding: 1rem;
+ margin-top: 1rem;
}
.dots-row {
@@ -270,7 +377,7 @@
flex-direction: row;
justify-content: center;
align-items: center;
- padding-bottom: 1rem;
+ margin: 1rem;
}
.dots-holder {
diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx
index 9a77e92f56e..99c9a6a531f 100644
--- a/src/components/connection-modal/connection-modal.jsx
+++ b/src/components/connection-modal/connection-modal.jsx
@@ -3,9 +3,10 @@ import React from 'react';
import keyMirror from 'keymirror';
import Box from '../box/box.jsx';
-import Modal from '../modal/modal.jsx';
+import Modal from '../../containers/modal.jsx';
import ScanningStep from '../../containers/scanning-step.jsx';
+import AutoScanningStep from '../../containers/auto-scanning-step.jsx';
import ConnectingStep from './connecting-step.jsx';
import ConnectedStep from './connected-step.jsx';
import ErrorStep from './error-step.jsx';
@@ -26,11 +27,13 @@ const ConnectionModalComponent = props => (
className={styles.modalContent}
contentLabel={props.name}
headerClassName={styles.header}
- headerImage={props.smallDeviceImage}
+ headerImage={props.smallPeripheralImage}
+ onHelp={props.onHelp}
onRequestClose={props.onCancel}
>
- {props.phase === PHASES.scanning && }
+ {props.phase === PHASES.scanning && !props.useAutoScan && }
+ {props.phase === PHASES.scanning && props.useAutoScan && }
{props.phase === PHASES.connecting && }
{props.phase === PHASES.connected && }
{props.phase === PHASES.error && }
@@ -40,11 +43,15 @@ const ConnectionModalComponent = props => (
);
ConnectionModalComponent.propTypes = {
+ connectingMessage: PropTypes.node,
name: PropTypes.node,
onCancel: PropTypes.func.isRequired,
+ onHelp: PropTypes.func.isRequired,
+ peripheralButtonImage: PropTypes.string,
phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired,
- smallDeviceImage: PropTypes.string,
- title: PropTypes.string.isRequired
+ smallPeripheralImage: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ useAutoScan: PropTypes.bool.isRequired
};
export {
diff --git a/src/components/connection-modal/error-step.jsx b/src/components/connection-modal/error-step.jsx
index 681d74b72a2..21dcd8d54c9 100644
--- a/src/components/connection-modal/error-step.jsx
+++ b/src/components/connection-modal/error-step.jsx
@@ -1,5 +1,6 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import React from 'react';
import Box from '../box/box.jsx';
@@ -13,10 +14,10 @@ const ErrorStep = props => (
-
+
@@ -26,7 +27,7 @@ const ErrorStep = props => (
(
onClick={props.onScanning}
>
(
@@ -68,9 +69,9 @@ const ErrorStep = props => (
);
ErrorStep.propTypes = {
- deviceImage: PropTypes.string.isRequired,
onHelp: PropTypes.func,
- onScanning: PropTypes.func
+ onScanning: PropTypes.func,
+ peripheralImage: PropTypes.string.isRequired
};
export default ErrorStep;
diff --git a/src/components/connection-modal/icons/bluetooth.svg b/src/components/connection-modal/icons/bluetooth.svg
new file mode 100644
index 00000000000..ebec155b439
--- /dev/null
+++ b/src/components/connection-modal/icons/bluetooth.svg
@@ -0,0 +1,31 @@
+
+
+
+ Bluetooth
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/connection-modal/icons/scratch-link.png b/src/components/connection-modal/icons/scratch-link.png
deleted file mode 100644
index af7ceed4baa..00000000000
Binary files a/src/components/connection-modal/icons/scratch-link.png and /dev/null differ
diff --git a/src/components/connection-modal/icons/scratchlink.svg b/src/components/connection-modal/icons/scratchlink.svg
new file mode 100644
index 00000000000..de2011a5efd
--- /dev/null
+++ b/src/components/connection-modal/icons/scratchlink.svg
@@ -0,0 +1,34 @@
+
+
+
+ scratchlink
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/connection-modal/icons/searching.png b/src/components/connection-modal/icons/searching.png
index 260f3227f2e..7a72005a102 100644
Binary files a/src/components/connection-modal/icons/searching.png and b/src/components/connection-modal/icons/searching.png differ
diff --git a/src/components/connection-modal/device-tile.jsx b/src/components/connection-modal/peripheral-tile.jsx
similarity index 77%
rename from src/components/connection-modal/device-tile.jsx
rename to src/components/connection-modal/peripheral-tile.jsx
index c806453aa0f..6b7e0388d62 100644
--- a/src/components/connection-modal/device-tile.jsx
+++ b/src/components/connection-modal/peripheral-tile.jsx
@@ -7,7 +7,7 @@ import Box from '../box/box.jsx';
import styles from './connection-modal.css';
-class DeviceTile extends React.Component {
+class PeripheralTile extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
@@ -19,26 +19,26 @@ class DeviceTile extends React.Component {
}
render () {
return (
-
-
+
+
-
-
+
+
-
+
{this.props.name}
-
+
(
{props.scanning ? (
- props.deviceList.length === 0 ? (
+ props.peripheralList.length === 0 ? (
) : (
-
- {props.deviceList.map(device =>
- (
+ {props.peripheralList.map(peripheral =>
+ ( )
)}
-
+
)
) : (
)}
@@ -84,19 +85,19 @@ const ScanningStep = props => (
);
ScanningStep.propTypes = {
- deviceList: PropTypes.arrayOf(PropTypes.shape({
+ onConnecting: PropTypes.func,
+ onRefresh: PropTypes.func,
+ peripheralList: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
rssi: PropTypes.number,
peripheralId: PropTypes.string
})),
- onConnecting: PropTypes.func,
- onRefresh: PropTypes.func,
scanning: PropTypes.bool.isRequired,
- smallDeviceImage: PropTypes.string
+ smallPeripheralImage: PropTypes.string
};
ScanningStep.defaultProps = {
- deviceList: [],
+ peripheralList: [],
scanning: true
};
diff --git a/src/components/connection-modal/unavailable-step.jsx b/src/components/connection-modal/unavailable-step.jsx
index 2f789cbdb0e..ae89404a195 100644
--- a/src/components/connection-modal/unavailable-step.jsx
+++ b/src/components/connection-modal/unavailable-step.jsx
@@ -1,35 +1,60 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import React from 'react';
import Box from '../box/box.jsx';
import Dots from './dots.jsx';
import helpIcon from './icons/help.svg';
import backIcon from './icons/back.svg';
-import scratchLinkIcon from './icons/scratch-link.png';
+import bluetoothIcon from './icons/bluetooth.svg';
+import scratchLinkIcon from './icons/scratchlink.svg';
import styles from './connection-modal.css';
const UnavailableStep = props => (
-
-
-
+
+
+
+ {'1'}
+
+
+
+
+
+
+
-
+
+
+ {'2'}
+
+
+
+
+
+
+
+
+
-
-
-
(
onClick={props.onScanning}
>
(
diff --git a/src/components/costume-canvas/costume-canvas.jsx b/src/components/costume-canvas/costume-canvas.jsx
deleted file mode 100644
index 4375572c1e2..00000000000
--- a/src/components/costume-canvas/costume-canvas.jsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import svgToImage from 'svg-to-image';
-import xhr from 'xhr';
-
-/**
- * @fileoverview
- * A component for rendering Scratch costume URLs to canvases.
- * Use for sprite library, costume library, sprite selector, etc.
- * Props include width, height, and direction (direction in Scratch value).
- */
-
-class CostumeCanvas extends React.Component {
- componentDidMount () {
- this.load();
- }
- componentDidUpdate (prevProps) {
- if (prevProps.url !== this.props.url) {
- this.load();
- } else if (
- prevProps.width !== this.props.width ||
- prevProps.height !== this.props.height ||
- prevProps.direction !== this.props.direction
- ) {
- this.draw();
- }
- }
- draw () {
- if (!this.canvas) {
- return;
- }
-
- // Draw the costume to the rendered canvas.
- const img = this.img;
- const context = this.canvas.getContext('2d');
-
- // Scale to fit.
- let scale;
-
- // Choose the larger dimension to scale by.
- if (img.width > img.height) {
- scale = this.canvas.width / img.width;
- } else {
- scale = this.canvas.height / img.height;
- }
-
- // Rotate by the Scratch-value direction.
- const angle = (-90 + this.props.direction) * Math.PI / 180;
-
- // Rotation origin point will be center of the canvas.
- const contextTranslateX = this.canvas.width / 2;
- const contextTranslateY = this.canvas.height / 2;
-
- // First, clear the canvas.
- context.clearRect(0, 0,
- this.canvas.width, this.canvas.height);
-
- // Translate the context to the center of the canvas,
- // then rotate canvas drawing by `angle`.
- context.translate(contextTranslateX, contextTranslateY);
- context.rotate(angle);
- context.drawImage(img,
- 0, 0, img.width, img.height,
- -(scale * img.width / 2), -(scale * img.height / 2),
- scale * img.width,
- scale * img.height);
-
- // Reset the canvas rotation and translation to 0, (0, 0).
- context.rotate(-angle);
- context.translate(-contextTranslateX, -contextTranslateY);
- }
- load () {
- // Draw the icon on our canvas.
- const url = this.props.url;
- if (url.indexOf('.svg') > -1) {
- // Vector graphics: need to download with XDR and rasterize.
- // Queue request asynchronously.
- setTimeout(() => {
- xhr.get({
- useXDR: true,
- url: url
- }, (err, response, body) => {
- if (!err) {
- svgToImage(body, (svgErr, img) => {
- if (!svgErr) {
- this.img = img;
- this.draw();
- }
- });
- }
- });
- }, 0);
-
- } else {
- // Raster graphics: create Image and draw it.
- const img = new Image();
- img.src = url;
- img.onload = () => {
- this.img = img;
- this.draw();
- };
- }
- }
- render () {
- return (
-
(this.canvas = c)} // eslint-disable-line react/jsx-sort-props
- />
- );
- }
-}
-
-CostumeCanvas.defaultProps = {
- width: 100,
- height: 100,
- direction: 90
-};
-
-CostumeCanvas.propTypes = {
- className: PropTypes.string,
- direction: PropTypes.number,
- height: PropTypes.number,
- url: PropTypes.string.isRequired,
- width: PropTypes.number
-};
-
-export default CostumeCanvas;
diff --git a/src/components/crash-message/crash-message.jsx b/src/components/crash-message/crash-message.jsx
index 8ea44473986..764c8e2167d 100644
--- a/src/components/crash-message/crash-message.jsx
+++ b/src/components/crash-message/crash-message.jsx
@@ -1,12 +1,7 @@
-/* eslint-disable react/jsx-no-literals */
-/*
- @todo Rule is disabled because this component is rendered outside the
- intl provider right now so cannot be translated.
-*/
-
import PropTypes from 'prop-types';
import React from 'react';
import Box from '../box/box.jsx';
+import {FormattedMessage} from 'react-intl';
import styles from './crash-message.css';
import reloadIcon from './reload.svg';
@@ -19,19 +14,30 @@ const CrashMessage = props => (
src={reloadIcon}
/>
- Oops! Something went wrong.
+
- We are so sorry, but it looks like Scratch has crashed. This bug has been
- automatically reported to the Scratch Team. Please refresh your page to try
- again.
-
+
- Reload
+
diff --git a/src/components/custom-procedures/custom-procedures.css b/src/components/custom-procedures/custom-procedures.css
index 6c8967a12d5..1716baab306 100644
--- a/src/components/custom-procedures/custom-procedures.css
+++ b/src/components/custom-procedures/custom-procedures.css
@@ -91,6 +91,10 @@
color: white;
}
-.button-row button + button {
+[dir="ltr"] .button-row button + button {
margin-left: 0.5rem;
}
+
+[dir="rtl"] .button-row button + button {
+ margin-right: 0.5rem;
+}
diff --git a/src/components/custom-procedures/custom-procedures.jsx b/src/components/custom-procedures/custom-procedures.jsx
index 08131ea5b1d..7d2f3e1a535 100644
--- a/src/components/custom-procedures/custom-procedures.jsx
+++ b/src/components/custom-procedures/custom-procedures.jsx
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
-import Modal from '../modal/modal.jsx';
+import Modal from '../../containers/modal.jsx';
import Box from '../box/box.jsx';
-import {FormattedMessage} from 'react-intl';
+import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
import booleanInputIcon from './icon--boolean-input.svg';
import textInputIcon from './icon--text-input.svg';
@@ -10,10 +10,18 @@ import labelIcon from './icon--label.svg';
import styles from './custom-procedures.css';
+const messages = defineMessages({
+ myblockModalTitle: {
+ defaultMessage: 'Make a Block',
+ description: 'Title for the modal where you create a custom block.',
+ id: 'gui.customProcedures.myblockModalTitle'
+ }
+});
+
const CustomProcedures = props => (
(
CustomProcedures.propTypes = {
componentRef: PropTypes.func.isRequired,
+ intl: intlShape,
onAddBoolean: PropTypes.func.isRequired,
onAddLabel: PropTypes.func.isRequired,
onAddTextNumber: PropTypes.func.isRequired,
@@ -142,4 +151,4 @@ CustomProcedures.propTypes = {
warp: PropTypes.bool.isRequired
};
-export default CustomProcedures;
+export default injectIntl(CustomProcedures);
diff --git a/src/components/custom-procedures/icon--label.svg b/src/components/custom-procedures/icon--label.svg
index 2c2d52a66da..c46c2f00cc8 100644
--- a/src/components/custom-procedures/icon--label.svg
+++ b/src/components/custom-procedures/icon--label.svg
@@ -1 +1 @@
-R1_ C.Procedure Editble Inputs text
\ No newline at end of file
+R1_ C.Procedure Editble Inputs text
diff --git a/src/components/direction-picker/dial.css b/src/components/direction-picker/dial.css
new file mode 100644
index 00000000000..ce9b7daea87
--- /dev/null
+++ b/src/components/direction-picker/dial.css
@@ -0,0 +1,41 @@
+@import "../../css/colors.css";
+
+.container {
+ padding: 1rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ user-select: none;
+}
+
+.dial-container {
+ position: relative;
+}
+
+.dial-face, .dial-handle, .gauge {
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: visible;
+}
+
+.dial-face {
+ width: 100%;
+}
+
+$dial-size: 40px;
+
+.dial-handle {
+ cursor: pointer;
+ width: $dial-size;
+ height: $dial-size;
+ /* Use margin to make positioning via top/left easier */
+ margin-left: calc($dial-size / -2);
+ margin-top: calc($dial-size / -2);
+}
+
+.gauge-path {
+ fill: $motion-transparent;
+ stroke: $motion-primary;
+ stroke-width: 1px;
+}
diff --git a/src/components/direction-picker/dial.jsx b/src/components/direction-picker/dial.jsx
new file mode 100644
index 00000000000..df97fda7fa4
--- /dev/null
+++ b/src/components/direction-picker/dial.jsx
@@ -0,0 +1,156 @@
+import PropTypes from 'prop-types';
+import bindAll from 'lodash.bindall';
+import React from 'react';
+import {getEventXY} from '../../lib/touch-utils';
+
+import styles from './dial.css';
+
+import dialFace from './icon--dial.svg';
+import dialHandle from './icon--handle.svg';
+
+class Dial extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleMouseDown',
+ 'handleMouseMove',
+ 'containerRef',
+ 'handleRef',
+ 'unbindMouseEvents'
+ ]);
+ }
+
+ componentDidMount () {
+ // Manually add touch/mouse handlers so that preventDefault can be used
+ // to prevent scrolling on touch.
+ // Tracked as a react issue https://github.com/facebook/react/issues/6436
+ this.handleElement.addEventListener('mousedown', this.handleMouseDown);
+ this.handleElement.addEventListener('touchstart', this.handleMouseDown);
+ }
+
+ componentWillUnmount () {
+ this.unbindMouseEvents();
+ this.handleElement.removeEventListener('mousedown', this.handleMouseDown);
+ this.handleElement.removeEventListener('touchstart', this.handleMouseDown);
+ }
+
+ /**
+ * Get direction from dial center to mouse move event.
+ * @param {Event} e - Mouse move event.
+ * @returns {number} Direction in degrees, clockwise, 90=horizontal.
+ */
+ directionToMouseEvent (e) {
+ const {x: mx, y: my} = getEventXY(e);
+ const bbox = this.containerElement.getBoundingClientRect();
+ const cy = bbox.top + (bbox.height / 2);
+ const cx = bbox.left + (bbox.width / 2);
+ const angle = Math.atan2(my - cy, mx - cx);
+ const degrees = angle * (180 / Math.PI);
+ return degrees + 90; // To correspond with scratch coordinate system
+ }
+
+ /**
+ * Create SVG path data string for the dial "gauge", the overlaid arc slice.
+ * @param {number} radius - The radius of the dial.
+ * @param {number} direction - Direction in degrees, clockwise, 90=horizontal.
+ * @returns {string} Path data string for the gauge.
+ */
+ gaugePath (radius, direction) {
+ const rads = (direction) * (Math.PI / 180);
+ const path = [];
+ path.push(`M ${radius} 0`);
+ path.push(`L ${radius} ${radius}`);
+ path.push(`L ${radius + (radius * Math.sin(rads))} ${radius - (radius * Math.cos(rads))}`);
+ path.push(`A ${radius} ${radius} 0 0 ${direction < 0 ? 1 : 0} ${radius} 0`);
+ path.push(`Z`);
+ return path.join(' ');
+ }
+
+ handleMouseMove (e) {
+ this.props.onChange(this.directionToMouseEvent(e) + this.directionOffset);
+ e.preventDefault();
+ }
+
+ unbindMouseEvents () {
+ window.removeEventListener('mousemove', this.handleMouseMove);
+ window.removeEventListener('mouseup', this.unbindMouseEvents);
+ window.removeEventListener('touchmove', this.handleMouseMove);
+ window.removeEventListener('touchend', this.unbindMouseEvents);
+ }
+
+ handleMouseDown (e) {
+ // Because the drag handle is not a single point, there is some initial
+ // difference between the current sprite direction and the direction to the mouse
+ // Store this offset to prevent jumping when the mouse is moved.
+ this.directionOffset = this.props.direction - this.directionToMouseEvent(e);
+ window.addEventListener('mousemove', this.handleMouseMove);
+ window.addEventListener('mouseup', this.unbindMouseEvents);
+ window.addEventListener('touchmove', this.handleMouseMove);
+ window.addEventListener('touchend', this.unbindMouseEvents);
+ e.preventDefault();
+ }
+
+ containerRef (el) {
+ this.containerElement = el;
+ }
+
+ handleRef (el) {
+ this.handleElement = el;
+ }
+
+ render () {
+ const {direction, radius} = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+Dial.propTypes = {
+ direction: PropTypes.number,
+ onChange: PropTypes.func.isRequired,
+ radius: PropTypes.number
+};
+
+Dial.defaultProps = {
+ direction: 90, // degrees
+ radius: 56 // px
+};
+
+export default Dial;
diff --git a/src/components/direction-picker/direction-picker.css b/src/components/direction-picker/direction-picker.css
new file mode 100644
index 00000000000..501ff021714
--- /dev/null
+++ b/src/components/direction-picker/direction-picker.css
@@ -0,0 +1,32 @@
+@import "../../css/colors.css";
+
+.button-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+
+}
+
+.icon-button {
+ margin: 0.25rem;
+ border: none;
+ background: none;
+ outline: none;
+ cursor: pointer;
+ user-select: none;
+}
+
+.icon-button:active > img {
+ width: 20px;
+ height: 20px;
+ transform: scale(1.15);
+}
+
+.icon-button > img {
+ transition: transform 0.1s;
+ filter: grayscale(100%);
+}
+
+.icon-button.active > img {
+ filter: none;
+}
diff --git a/src/components/direction-picker/direction-picker.jsx b/src/components/direction-picker/direction-picker.jsx
new file mode 100644
index 00000000000..84a15db7a7e
--- /dev/null
+++ b/src/components/direction-picker/direction-picker.jsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import Popover from 'react-popover';
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+
+import Label from '../forms/label.jsx';
+import Input from '../forms/input.jsx';
+import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
+import Dial from './dial.jsx';
+
+import styles from './direction-picker.css';
+
+import allAroundIcon from './icon--all-around.svg';
+import leftRightIcon from './icon--left-right.svg';
+import dontRotateIcon from './icon--dont-rotate.svg';
+
+const BufferedInput = BufferedInputHOC(Input);
+
+const directionLabel = (
+
+);
+
+const RotationStyles = {
+ ALL_AROUND: 'all around',
+ LEFT_RIGHT: 'left-right',
+ DONT_ROTATE: "don't rotate"
+};
+
+const messages = defineMessages({
+ allAround: {
+ id: 'gui.directionPicker.rotationStyles.allAround',
+ description: 'Button to change to the all around rotation style',
+ defaultMessage: 'All Around'
+ },
+ leftRight: {
+ id: 'gui.directionPicker.rotationStyles.leftRight',
+ description: 'Button to change to the left-right rotation style',
+ defaultMessage: 'Left/Right'
+ },
+ dontRotate: {
+ id: 'gui.directionPicker.rotationStyles.dontRotate',
+ description: 'Button to change to the dont rotate rotation style',
+ defaultMessage: 'Do not rotate'
+ }
+});
+
+const DirectionPicker = props => (
+
+
+
+
+
+ }
+ isOpen={props.popoverOpen}
+ preferPlace="above"
+ onOuterAction={props.onClosePopover}
+ >
+
+
+
+
+);
+
+DirectionPicker.propTypes = {
+ direction: PropTypes.number,
+ disabled: PropTypes.bool.isRequired,
+ intl: intlShape,
+ labelAbove: PropTypes.bool,
+ onChangeDirection: PropTypes.func.isRequired,
+ onClickAllAround: PropTypes.func.isRequired,
+ onClickDontRotate: PropTypes.func.isRequired,
+ onClickLeftRight: PropTypes.func.isRequired,
+ onClosePopover: PropTypes.func.isRequired,
+ onOpenPopover: PropTypes.func.isRequired,
+ popoverOpen: PropTypes.bool.isRequired,
+ rotationStyle: PropTypes.string
+};
+
+DirectionPicker.defaultProps = {
+ labelAbove: false
+};
+
+const WrappedDirectionPicker = injectIntl(DirectionPicker);
+
+export {
+ WrappedDirectionPicker as default,
+ RotationStyles
+};
diff --git a/src/components/direction-picker/icon--all-around.svg b/src/components/direction-picker/icon--all-around.svg
new file mode 100644
index 00000000000..2412c0b2327
--- /dev/null
+++ b/src/components/direction-picker/icon--all-around.svg
@@ -0,0 +1,10 @@
+
+
+
+ all-around-active
+ Created with Sketch.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/direction-picker/icon--dial.svg b/src/components/direction-picker/icon--dial.svg
new file mode 100644
index 00000000000..d4aa8ed8205
--- /dev/null
+++ b/src/components/direction-picker/icon--dial.svg
@@ -0,0 +1 @@
+
dial-face
\ No newline at end of file
diff --git a/src/components/direction-picker/icon--dont-rotate.svg b/src/components/direction-picker/icon--dont-rotate.svg
new file mode 100644
index 00000000000..4796c03af25
--- /dev/null
+++ b/src/components/direction-picker/icon--dont-rotate.svg
@@ -0,0 +1,10 @@
+
+
+
+ dont-rotate-active
+ Created with Sketch.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/direction-picker/icon--handle.svg b/src/components/direction-picker/icon--handle.svg
new file mode 100644
index 00000000000..8e5fee6e0ba
--- /dev/null
+++ b/src/components/direction-picker/icon--handle.svg
@@ -0,0 +1 @@
+
01_Dial Elements
\ No newline at end of file
diff --git a/src/components/direction-picker/icon--left-right.svg b/src/components/direction-picker/icon--left-right.svg
new file mode 100644
index 00000000000..4525bd6e917
--- /dev/null
+++ b/src/components/direction-picker/icon--left-right.svg
@@ -0,0 +1,14 @@
+
+
+
+ left-right-active
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/filter/filter.css b/src/components/filter/filter.css
index b67e60795c2..fffef4812ec 100644
--- a/src/components/filter/filter.css
+++ b/src/components/filter/filter.css
@@ -18,13 +18,22 @@
.filter-icon {
position: absolute;
top: 0;
- left: 0;
height: 1rem;
width: 1rem;
+}
+
+[dir="ltr"] .filter-icon {
+ left: 0;
margin: 0.75rem 0.75rem 0.75rem 1rem;
}
+[dir="rtl"] .filter-icon {
+ right: 0;
+ margin: 0.75rem 1rem 0.75rem 0.75rem;
+ transform: scaleX(-1);
+}
+
.filter:focus-within {
box-shadow: 0 0 0 .25rem $motion-transparent;
}
@@ -36,7 +45,6 @@
opacity: 0;
position: absolute;
top: 0;
- right: 0;
display: flex;
justify-content: center;
@@ -53,6 +61,14 @@
transition: opacity 0.05s linear;
}
+[dir="ltr"] .x-icon-wrapper {
+ right: 0;
+}
+
+[dir="rtl"] .x-icon-wrapper {
+ left: 0;
+}
+
/*
Shown state
*/
@@ -96,13 +112,27 @@
font-size: 0.75rem;
letter-spacing: 0.15px;
cursor: text;
+}
+
+[dir="ltr"] .filter-input {
padding: .625rem 2rem .625rem 3rem;
}
+[dir="rtl"] .filter-input {
+ padding: .625rem 3rem .625rem 2rem;
+}
+
.filter-input::placeholder {
opacity: .5;
- padding: 0 0 0 0.25rem;
color: $text-primary;
font-size: 0.875rem;
letter-spacing: 0.15px;
}
+
+[dir="ltr"] .filter-input::placeholder {
+ padding: 0 0 0 0.25rem;
+}
+
+[dir="rtl"] .filter-input::placeholder {
+ padding: 0 0.25rem 0 0;
+}
diff --git a/src/components/forms/label.css b/src/components/forms/label.css
index 759a731d37e..ec27274c7f7 100644
--- a/src/components/forms/label.css
+++ b/src/components/forms/label.css
@@ -7,15 +7,32 @@
align-items: center;
}
+.input-group-column {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.input-group-column span {
+ margin-bottom: .25rem;
+}
+
.input-label, .input-label-secondary {
font-size: 0.625rem;
- margin-right: .5rem;
user-select: none;
cursor: default;
white-space: nowrap;
}
+[dir="ltr"] .input-label, [dir="ltr"] .input-label-secondary {
+ margin-right: .5rem;
+}
+
+[dir="rtl"] .input-label, [dir="rtl"] .input-label-secondary {
+ margin-left: .5rem;
+}
+
.input-label {
font-weight: bold;
}
diff --git a/src/components/forms/label.jsx b/src/components/forms/label.jsx
index e005844587e..8b90ad1e899 100644
--- a/src/components/forms/label.jsx
+++ b/src/components/forms/label.jsx
@@ -4,7 +4,7 @@ import React from 'react';
import styles from './label.css';
const Label = props => (
-
+
{props.text}
@@ -13,12 +13,14 @@ const Label = props => (
);
Label.propTypes = {
+ above: PropTypes.bool,
children: PropTypes.node,
secondary: PropTypes.bool,
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
};
Label.defaultProps = {
+ above: false,
secondary: false
};
diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css
index b53f17b55eb..16f79ea4d68 100644
--- a/src/components/gui/gui.css
+++ b/src/components/gui/gui.css
@@ -63,7 +63,6 @@
flex-grow: 1;
height: 80%;
margin-bottom: 0;
- margin-left: -0.5rem;
border-radius: 1rem 1rem 0 0;
border: 1px solid $ui-black-transparent;
@@ -82,9 +81,24 @@
white-space: nowrap;
}
+[dir="ltr"] .tab {
+ margin-left: -0.5rem;
+}
+
+[dir="rtl"] .tab {
+ margin-right: -0.5rem;
+}
+
+[dir="ltr"] .tab:nth-of-type(1) {
+ margin-left: 0;
+}
+
+[dir="rtl"] .tab:nth-of-type(1) {
+ margin-right: 0;
+}
+
/* Use z-indices to force left-on-top for tabs */
.tab:nth-of-type(1) {
- margin-left: 0;
z-index: 3;
}
.tab:nth-of-type(2) {
@@ -106,11 +120,27 @@
}
.tab img {
- margin-right: 0.125rem;
width: 1.375rem;
filter: grayscale(100%);
}
+[dir="ltr"] .tab img {
+ margin-right: 0.125rem;
+}
+
+[dir="rtl"] .tab img {
+ margin-left: 0.125rem;
+}
+
+/* mirror blocks and sound tab icons */
+[dir="rtl"] .tab:nth-of-type(1) img {
+ transform: scaleX(-1);
+}
+
+[dir="rtl"] .tab:nth-of-type(3) img {
+ transform: scaleX(-1);
+}
+
.tab.is-selected img {
filter: none;
}
@@ -164,6 +194,9 @@
*/
display: flex;
flex-direction: column;
+ /* pad entire wrapper to the left and right; allow children to fill width */
+ padding-left: $space;
+ padding-right: $space;
}
.stage-and-target-wrapper.large {
@@ -187,11 +220,8 @@
flex-basis: 0;
padding-top: $space;
- padding-left: $space;
- padding-right: $space;
-
min-height: 0; /* this makes it work in Firefox */
-
+
/*
For making the sprite-selector a scrollable pane
@todo: Not working in Safari
@@ -201,11 +231,12 @@
}
.extension-button-container {
- width: 3.25rem;
+ width: 3.75rem;
height: 3.25rem;
position: absolute;
bottom: 0;
left: 0;
+ right: 0;
z-index: $z-index-extension-button;
background: $motion-primary;
@@ -240,6 +271,22 @@ $fade-out-distance: 15px;
height: 1.75rem;
}
+[dir="rtl"] .extension-button-icon {
+ transform: scaleX(-1);
+}
+
.extension-button > div {
margin-top: 0;
}
+
+/* Alerts */
+
+.alerts-container {
+ width: 448px;
+ z-index: $z-index-alerts;
+ left: 0;
+ right: 0;
+ margin: auto;
+ position: absolute;
+ margin-top: 53px;
+}
diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 8d251bd14e1..9cdafaa8cfb 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -27,6 +27,7 @@ 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';
+import Alerts from '../../containers/alerts.jsx';
import DragLayer from '../../containers/drag-layer.jsx';
import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants';
@@ -52,7 +53,9 @@ let isRendererSupported = null;
const GUIComponent = props => {
const {
+ accountNavOpen,
activeTabIndex,
+ alertsVisible,
basePath,
backdropLibraryVisible,
backpackOptions,
@@ -65,14 +68,23 @@ const GUIComponent = props => {
importInfoVisible,
intl,
isPlayerOnly,
+ isRtl,
loading,
- onExtensionButtonClick,
+ renderLogin,
+ onClickAccountNav,
+ onCloseAccountNav,
+ onLogOut,
+ onOpenRegistration,
+ onToggleLoginOpen,
+ onUpdateProjectTitle,
onActivateCostumesTab,
onActivateSoundsTab,
onActivateTab,
+ onExtensionButtonClick,
onRequestCloseBackdropLibrary,
onRequestCloseCostumeLibrary,
onSeeCommunity,
+ onShare,
previewInfoVisible,
targetIsStage,
soundsTabVisible,
@@ -106,10 +118,15 @@ const GUIComponent = props => {
isRendererSupported={isRendererSupported}
stageSize={stageSize}
vm={vm}
- />
+ >
+ {alertsVisible ? (
+
+ ) : null}
+
) : (
{previewInfoVisible ? (
@@ -122,7 +139,7 @@ const GUIComponent = props => {
) : null}
{isRendererSupported ? null : (
-
+
)}
{tipsLibraryVisible ? (
@@ -130,6 +147,9 @@ const GUIComponent = props => {
{cardsVisible ? (
) : null}
+ {alertsVisible ? (
+
+ ) : null}
{costumeLibraryVisible ? (
{
/>
) : null}
@@ -266,6 +295,7 @@ const GUIComponent = props => {
};
GUIComponent.propTypes = {
+ accountNavOpen: PropTypes.bool,
activeTabIndex: PropTypes.number,
backdropLibraryVisible: PropTypes.bool,
backpackOptions: PropTypes.shape({
@@ -282,16 +312,25 @@ GUIComponent.propTypes = {
importInfoVisible: PropTypes.bool,
intl: intlShape.isRequired,
isPlayerOnly: PropTypes.bool,
+ isRtl: PropTypes.bool,
loading: PropTypes.bool,
onActivateCostumesTab: PropTypes.func,
onActivateSoundsTab: PropTypes.func,
onActivateTab: PropTypes.func,
+ onClickAccountNav: PropTypes.func,
+ onCloseAccountNav: PropTypes.func,
onExtensionButtonClick: PropTypes.func,
+ onLogOut: PropTypes.func,
+ onOpenRegistration: PropTypes.func,
onRequestCloseBackdropLibrary: PropTypes.func,
onRequestCloseCostumeLibrary: PropTypes.func,
onSeeCommunity: PropTypes.func,
+ onShare: PropTypes.func,
onTabSelect: PropTypes.func,
+ onToggleLoginOpen: PropTypes.func,
+ onUpdateProjectTitle: PropTypes.func,
previewInfoVisible: PropTypes.bool,
+ renderLogin: PropTypes.func,
soundsTabVisible: PropTypes.bool,
stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)),
targetIsStage: PropTypes.bool,
diff --git a/src/components/import-modal/import-modal.css b/src/components/import-modal/import-modal.css
index b265efae02f..7da215c015b 100644
--- a/src/components/import-modal/import-modal.css
+++ b/src/components/import-modal/import-modal.css
@@ -82,6 +82,10 @@ $sides: 20rem;
justify-content: flex-start;
}
+[dir="rtl"] .header-item-close img {
+ transform: scaleX(-1);
+}
+
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx
index ad42929e7d6..60b13021228 100644
--- a/src/components/import-modal/import-modal.jsx
+++ b/src/components/import-modal/import-modal.jsx
@@ -16,6 +16,12 @@ const messages = defineMessages({
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',
@@ -36,106 +42,112 @@ const ImportModal = ({intl, ...props}) => (
overlayClassName={styles.modalOverlay}
onRequestClose={props.onCancel}
>
-
-
-
-
-
-
-
- {intl.formatMessage({...messages.title})}
-
-
-
- {null}
+
+
+
+
+
+
+
+
+ {intl.formatMessage({...messages.title})}
+
+
+
+ {null}
+
-
-
+
-
-
- {intl.formatMessage({...messages.formDescription})}
-
-
-
-
+
+ {intl.formatMessage({...messages.formDescription})}
+
+
-
-
-
- {props.hasValidationError ?
-
-
+
-
- : null
- }
-
-
+
+
+ {props.hasValidationError ?
+
+
+
+
+ : null
+ }
+
+
+
+
+
+
+
+
+ )
+ }}
/>
-
-
-
-
-
-
- )
- }}
- />
+
-
+
);
@@ -144,6 +156,7 @@ ImportModal.propTypes = {
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,
diff --git a/src/components/language-selector/language-selector.css b/src/components/language-selector/language-selector.css
index d8ae588ae13..25b9951608e 100644
--- a/src/components/language-selector/language-selector.css
+++ b/src/components/language-selector/language-selector.css
@@ -5,19 +5,27 @@
height: 1.5rem;
}
-.disabled {
- opacity: .5;
-}
-
+/* Position the language select over the language icon, and make it transparent */
.language-select {
- margin: .5rem;
- height: 1.85rem;
- border: 1px solid $motion-primary;
+ position: absolute;
+ width: $language-selector-width;
+ height: $menu-bar-height;
+ opacity: 0;
user-select: none;
- outline: none;
- background: rgba(255, 255, 255, 0.5);
- color: $motion-tertiary;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: .875rem;
+}
+
+[dir="ltr"] .language-select {
+ right: 0;
+}
+
+[dir="rtl"] .language-select {
+ left: 0;
+}
+
+.language-select option {
+ opacity: 1;
}
.language-select:focus {
diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx
index ad7a9aa90f9..864c4848fd7 100644
--- a/src/components/language-selector/language-selector.jsx
+++ b/src/components/language-selector/language-selector.jsx
@@ -1,51 +1,38 @@
import PropTypes from 'prop-types';
import React from 'react';
-import Box from '../box/box.jsx';
import locales from 'scratch-l10n';
import styles from './language-selector.css';
-class LanguageSelector extends React.Component {
- render () {
- const {
- componentRef,
- currentLocale,
- onChange,
- ...componentProps
- } = this.props;
- return (
-
-
- (
+
+ {
+ Object.keys(locales)
+ .filter(l => !ignore.includes(l))
+ .map(locale => (
+
- {Object.keys(locales).map(locale => (
-
- {locales[locale].name}
-
- ))}
-
-
-
- );
- }
-}
+ {locales[locale].name}
+
+ ))
+ }
+
+);
LanguageSelector.propTypes = {
- componentRef: PropTypes.func,
currentLocale: PropTypes.string,
- onChange: PropTypes.func,
- open: PropTypes.bool
+ label: PropTypes.string,
+ onChange: PropTypes.func
};
export default LanguageSelector;
diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css
index 3258b4a7bd1..fd7217c6744 100644
--- a/src/components/library-item/library-item.css
+++ b/src/components/library-item/library-item.css
@@ -99,7 +99,6 @@
.coming-soon-text {
position: absolute;
- transform: translate(calc(2 * $space), calc(2 * $space));
background-color: $data-primary;
border-radius: 1rem;
box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25);
@@ -108,3 +107,11 @@
font-weight: bold;
color: $ui-white;
}
+
+[dir="ltr"] .coming-soon-text {
+ transform: translate(calc(2 * $space), calc(2 * $space));
+}
+
+[dir="rtl"] .coming-soon-text {
+ transform: translate(calc(-2 * $space), calc(2 * $space));
+}
diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx
index c58d3608c94..9ff4ead42b3 100644
--- a/src/components/library/library.jsx
+++ b/src/components/library/library.jsx
@@ -9,25 +9,32 @@ import Modal from '../../containers/modal.jsx';
import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
import TagButton from '../../containers/tag-button.jsx';
+import analytics from '../../lib/analytics';
import styles from './library.css';
-const ALL_TAG_TITLE = 'All';
-const tagListPrefix = [{title: ALL_TAG_TITLE}];
-
const messages = defineMessages({
filterPlaceholder: {
id: 'gui.library.filterPlaceholder',
defaultMessage: 'Search',
description: 'Placeholder text for library search field'
+ },
+ allTag: {
+ id: 'gui.library.allTag',
+ defaultMessage: 'All',
+ description: 'Label for library tag to revert to all items after filtering by tag.'
}
});
+const ALL_TAG = {tag: 'all', intlLabel: messages.allTag};
+const tagListPrefix = [ALL_TAG];
+
class LibraryComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleBlur',
+ 'handleClose',
'handleFilterChange',
'handleFilterClear',
'handleFocus',
@@ -40,7 +47,7 @@ class LibraryComponent extends React.Component {
this.state = {
selectedItem: null,
filterQuery: '',
- selectedTag: ALL_TAG_TITLE.toLowerCase()
+ selectedTag: ALL_TAG.tag
};
}
componentDidUpdate (prevProps, prevState) {
@@ -56,9 +63,13 @@ class LibraryComponent extends React.Component {
this.handleMouseEnter(id);
}
handleSelect (id) {
- this.props.onRequestClose();
+ this.handleClose();
this.props.onItemSelected(this.getFilteredData()[id]);
}
+ handleClose () {
+ this.props.onRequestClose();
+ analytics.pageview(`/${this.props.id}/search?q=${this.state.filterQuery}`);
+ }
handleTagClick (tag) {
this.setState({
filterQuery: '',
@@ -74,7 +85,7 @@ class LibraryComponent extends React.Component {
handleFilterChange (event) {
this.setState({
filterQuery: event.target.value,
- selectedTag: ALL_TAG_TITLE.toLowerCase()
+ selectedTag: ALL_TAG.tag
});
}
handleFilterClear () {
@@ -111,7 +122,7 @@ class LibraryComponent extends React.Component {
fullScreen
contentLabel={this.props.title}
id={this.props.id}
- onRequestClose={this.props.onRequestClose}
+ onRequestClose={this.handleClose}
>
{(this.props.filterable || this.props.tags) && (
@@ -135,7 +146,7 @@ class LibraryComponent extends React.Component {
{tagListPrefix.concat(this.props.tags).map((tagProps, id) => (
(
+
+
+ {thumbnailUrl ? (
+
+ ) : null}
+
+ {username}
+
+
+
+
+
+
+
+
+
+
+
+
+ {isEducator ? (
+
+
+
+ ) : null}
+ {isStudent ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+);
+
+AccountNavComponent.propTypes = {
+ className: PropTypes.string,
+ classroomId: PropTypes.string,
+ isEducator: PropTypes.bool,
+ isOpen: PropTypes.bool,
+ isRtl: PropTypes.bool,
+ isStudent: PropTypes.bool,
+ menuBarMenuClassName: PropTypes.string,
+ onClick: PropTypes.func,
+ onClose: PropTypes.func,
+ onLogOut: PropTypes.func,
+ profileUrl: PropTypes.string,
+ thumbnailUrl: PropTypes.string,
+ username: PropTypes.string
+};
+
+export default AccountNavComponent;
diff --git a/src/components/language-selector/dropdown-caret.svg b/src/components/menu-bar/dropdown-caret.svg
similarity index 100%
rename from src/components/language-selector/dropdown-caret.svg
rename to src/components/menu-bar/dropdown-caret.svg
diff --git a/src/components/menu-bar/login-dropdown.css b/src/components/menu-bar/login-dropdown.css
new file mode 100644
index 00000000000..8b16b7fdbfe
--- /dev/null
+++ b/src/components/menu-bar/login-dropdown.css
@@ -0,0 +1,4 @@
+
+.login {
+ padding: .625rem;
+}
diff --git a/src/components/menu-bar/login-dropdown.jsx b/src/components/menu-bar/login-dropdown.jsx
new file mode 100644
index 00000000000..11d23856c8f
--- /dev/null
+++ b/src/components/menu-bar/login-dropdown.jsx
@@ -0,0 +1,50 @@
+/*
+NOTE: this file only temporarily resides in scratch-gui.
+Nearly identical code appears in scratch-www, and the two should
+eventually be consolidated.
+*/
+
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import MenuBarMenu from './menu-bar-menu.jsx';
+
+import styles from './login-dropdown.css';
+
+const LoginDropdown = ({
+ className,
+ isOpen,
+ isRtl,
+ onClose,
+ renderLogin
+}) => (
+
+
+ {renderLogin({
+ onClose: onClose
+ })}
+
+
+);
+
+LoginDropdown.propTypes = {
+ className: PropTypes.string,
+ isOpen: PropTypes.bool,
+ isRtl: PropTypes.bool,
+ onClose: PropTypes.func,
+ renderLogin: PropTypes.func
+};
+
+export default LoginDropdown;
diff --git a/src/components/menu-bar/menu-bar-menu.jsx b/src/components/menu-bar/menu-bar-menu.jsx
new file mode 100644
index 00000000000..d71d9e04930
--- /dev/null
+++ b/src/components/menu-bar/menu-bar-menu.jsx
@@ -0,0 +1,31 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Menu from '../../containers/menu.jsx';
+
+const MenuBarMenu = ({
+ children,
+ className,
+ onRequestClose,
+ open,
+ place = 'right'
+}) => (
+
+
+ {children}
+
+
+);
+
+MenuBarMenu.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ onRequestClose: PropTypes.func,
+ open: PropTypes.bool,
+ place: PropTypes.oneOf(['left', 'right'])
+};
+
+export default MenuBarMenu;
diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css
index 45669f8d6ed..dcc23067249 100644
--- a/src/components/menu-bar/menu-bar.css
+++ b/src/components/menu-bar/menu-bar.css
@@ -37,6 +37,7 @@
justify-content: flex-start;
flex-wrap: nowrap;
align-items: center;
+ flex-grow: 1;
}
.scratch-logo {
@@ -48,13 +49,13 @@
height: 1.5rem;
}
-.language-menu {
- display: inline-flex;
+.language-caret {
+ margin-bottom: .625rem;
}
-.menu {
- z-index: $z-index-menu-bar;
- top: $menu-bar-height;
+.language-menu {
+ display: inline-flex;
+ width: $language-selector-width;
}
.menu-bar-item {
@@ -66,6 +67,7 @@
align-self: center;
position: relative;
align-items: center;
+ white-space: nowrap;
height: $menu-bar-height;
}
@@ -78,6 +80,16 @@
background-color: $ui-black-transparent;
}
+.menu-bar-item.growable {
+ max-width: 12rem;
+ flex: 1;
+}
+
+.title-field-growable {
+ flex-grow: 1;
+ width: 2rem;
+}
+
.file-group {
display: flex;
flex-direction: row;
@@ -88,6 +100,11 @@
padding: 0 0.75rem;
}
+.menu-bar-menu {
+ margin-top: $menu-bar-height;
+ z-index: $z-index-menu-bar;
+}
+
.feedback-link {
color: $motion-primary;
text-decoration: none;
@@ -103,22 +120,6 @@
height: 34px;
}
-.title-field {
- border: 1px dashed $ui-black-transparent;
- border-radius: .25rem;
- width: 12rem;
- height: 34px;
- background-color: transparent;
- padding: .5rem;
-}
-
-.title-field,
-.title-field::placeholder {
- color: $ui-white;
- font-weight: bold;
- font-size: .8rem;
-}
-
.share-button {
background: $data-primary;
height: 32px;
@@ -138,13 +139,16 @@
opacity: 0.5;
}
-.account-info-wrapper {
+.account-info-group {
display: flex;
flex-direction: row;
- padding: 0 .5rem;
align-items: center;
}
+.account-info-group .menu-bar-item {
+ padding: 0 0.75rem;
+}
+
.mystuff-icon {
margin: 0 .25rem;
height: 1rem;
@@ -168,7 +172,50 @@
}
.dropdown-caret-icon {
- margin-left: .5rem;
width: 0.5rem;
height: 0.5rem;
}
+
+[dir="ltr"] .dropdown-caret-icon {
+ margin-left: .5rem;
+}
+
+[dir="rtl"] .dropdown-caret-icon {
+ margin-right: .5rem;
+}
+
+.disabled {
+ opacity: 0.5;
+}
+
+.save-in-progress {
+ animation: hue-rotate 3s linear infinite;
+}
+
+@keyframes hue-rotate {
+ from {
+ filter: hue-rotate();
+ }
+ to {
+ filter: hue-rotate(360deg);
+ }
+}
+
+.mystuff > a {
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 45%;
+ padding-right: 10px;
+ padding-left: 10px;
+ width: 30px;
+ overflow: hidden;
+ text-indent: 50px;
+ white-space: nowrap;
+}
+.mystuff > a:hover {
+ background-size: 50%;
+}
+
+.mystuff > a {
+ background-image: url("/images/mystuff.png");
+}
diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index b843e05b45a..7d321ede9ec 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -11,13 +11,21 @@ import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import Divider from '../divider/divider.jsx';
import LanguageSelector from '../../containers/language-selector.jsx';
import ProjectLoader from '../../containers/project-loader.jsx';
-import Menu from '../../containers/menu.jsx';
+import MenuBarMenu from './menu-bar-menu.jsx';
import {MenuItem, MenuSection} from '../menu/menu.jsx';
+import ProjectTitleInput from './project-title-input.jsx';
+import AccountNav from '../../containers/account-nav.jsx';
+import LoginDropdown from './login-dropdown.jsx';
import ProjectSaver from '../../containers/project-saver.jsx';
+import DeletionRestorer from '../../containers/deletion-restorer.jsx';
+import TurboMode from '../../containers/turbo-mode.jsx';
import {openTipsLibrary} from '../../reducers/modals';
import {setPlayer} from '../../reducers/mode';
import {
+ openAccountMenu,
+ closeAccountMenu,
+ accountMenuOpen,
openFileMenu,
closeFileMenu,
fileMenuOpen,
@@ -26,7 +34,10 @@ import {
editMenuOpen,
openLanguageMenu,
closeLanguageMenu,
- languageMenuOpen
+ languageMenuOpen,
+ openLoginMenu,
+ closeLoginMenu,
+ loginMenuOpen
} from '../../reducers/menus';
import styles from './menu-bar.css';
@@ -36,7 +47,7 @@ import mystuffIcon from './icon--mystuff.png';
import feedbackIcon from './icon--feedback.svg';
import profileIcon from './icon--profile.png';
import communityIcon from './icon--see-community.svg';
-import dropdownCaret from '../language-selector/dropdown-caret.svg';
+import dropdownCaret from './dropdown-caret.svg';
import languageIcon from '../language-selector/language-icon.svg';
import scratchLogo from './scratch-logo.svg';
@@ -89,10 +100,11 @@ MenuBarItemTooltip.propTypes = {
place: PropTypes.oneOf(['top', 'bottom', 'left', 'right'])
};
-const MenuItemTooltip = ({id, children, className}) => (
+const MenuItemTooltip = ({id, isRtl, children, className}) => (
@@ -103,88 +115,135 @@ const MenuItemTooltip = ({id, children, className}) => (
MenuItemTooltip.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
- id: PropTypes.string
+ id: PropTypes.string,
+ isRtl: PropTypes.bool
};
-const MenuBarMenu = ({
- children,
- onRequestClose,
- open,
- place = 'right'
-}) => (
-
- {children}
-
-);
-
-MenuBarMenu.propTypes = {
- children: PropTypes.node,
- onRequestClose: PropTypes.func,
- open: PropTypes.bool,
- place: PropTypes.oneOf(['left', 'right'])
-};
class MenuBar extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
- 'handleLanguageMouseUp'
+ 'handleLanguageMouseUp',
+ 'handleRestoreOption',
+ 'handleCloseFileMenuAndThen',
+ 'restoreOptionMessage'
]);
+ this.state = {projectSaveInProgress: false};
}
handleLanguageMouseUp (e) {
if (!this.props.languageMenuOpen) {
this.props.onClickLanguage(e);
}
}
+ handleRestoreOption (restoreFun) {
+ return () => {
+ restoreFun();
+ this.props.onRequestCloseEdit();
+ };
+ }
+ handleUpdateProject (updateFun) {
+ return () => {
+ this.props.onRequestCloseFile();
+ this.setState({projectSaveInProgress: true},
+ () => {
+ updateFun().then(() => {
+ this.setState({projectSaveInProgress: false});
+ });
+ }
+ );
+ };
+ }
+ handleCloseFileMenuAndThen (fn) {
+ return () => {
+ this.props.onRequestCloseFile();
+ fn();
+ };
+ }
+ restoreOptionMessage (deletedItem) {
+ switch (deletedItem) {
+ case 'Sprite':
+ return ();
+ case 'Sound':
+ return ();
+ case 'Costume':
+ return ();
+ default: {
+ return ();
+ }
+ }
+ }
render () {
+ const saveNowMessage = (
+
+ );
+ const shareButton = (
+
+
+
+ );
return (
-
+
-
+
+
+
+
+
-
-
-
+
-
+
-
-
-
-
-
-
+ {(saveProject, updateProject) => (
+ this.props.canUpdateProject ? (
+
+ {saveNowMessage}
+
+ ) : (
+
+ {saveNowMessage}
+
+ )
+ )}
+
+ />
+
@@ -245,10 +316,9 @@ class MenuBar extends React.Component {
{renderFileInput()}
)}
- {(saveProject, saveProps) => (
+ {saveProject => (
-
-
-
+ {(handleRestore, {restorable, deletedItem}) => (
+
+ {this.restoreOptionMessage(deletedItem)}
-
-
-
-
-
-
+ )}
-
-
-
+ {(toggleTurboMode, {turboMode}) => (
+
+ {turboMode ? (
+
+ ) : (
+
+ )}
-
+ )}
@@ -322,25 +392,23 @@ class MenuBar extends React.Component {
-
-
-
-
-
-
+ {this.props.onShare ? shareButton : (
+
+ {shareButton}
+
+ )}
{this.props.enableCommunity ?
@@ -372,64 +440,145 @@ class MenuBar extends React.Component {
}
-
-
-
-
-
-
-
-
-
-
-
- {'scratch-cat' /* @todo username */}
-
-
-
-
+
+ {/* show the proper UI in the account menu, given whether the user is
+ logged in, and whether a session is available to log in with */}
+
+ {this.props.sessionExists ? (
+ this.props.username ? (
+ // ************ user is logged in ************
+
+
+
+
+
+
+
+
+ ) : (
+ // ********* user not logged in, but a session exists
+ // ********* so they can choose to log in
+
+
+
+
+
+
+
+
+
+ )
+ ) : (
+ // ******** no login session is available, so don't show login stuff
+
+
+
+
+
+
+
+
+
+
+
+ {'scratch-cat'}
+
+
+
+
+
+ )}
);
@@ -437,35 +586,61 @@ class MenuBar extends React.Component {
}
MenuBar.propTypes = {
+ accountMenuOpen: PropTypes.bool,
+ canUpdateProject: PropTypes.bool,
editMenuOpen: PropTypes.bool,
enableCommunity: PropTypes.bool,
fileMenuOpen: PropTypes.bool,
intl: intlShape,
+ isRtl: PropTypes.bool,
languageMenuOpen: PropTypes.bool,
+ loginMenuOpen: PropTypes.bool,
+ onClickAccount: PropTypes.func,
onClickEdit: PropTypes.func,
onClickFile: PropTypes.func,
onClickLanguage: PropTypes.func,
+ onClickLogin: PropTypes.func,
+ onLogOut: PropTypes.func,
+ onOpenRegistration: PropTypes.func,
onOpenTipLibrary: PropTypes.func,
+ onRequestCloseAccount: PropTypes.func,
onRequestCloseEdit: PropTypes.func,
onRequestCloseFile: PropTypes.func,
onRequestCloseLanguage: PropTypes.func,
- onSeeCommunity: PropTypes.func
+ onRequestCloseLogin: PropTypes.func,
+ onSeeCommunity: PropTypes.func,
+ onToggleLoginOpen: PropTypes.func,
+ onUpdateProjectTitle: PropTypes.func,
+ renderLogin: PropTypes.func,
+ sessionExists: PropTypes.bool,
+ username: PropTypes.string
};
const mapStateToProps = state => ({
+ canUpdateProject: typeof (state.session && state.session.session && state.session.session.user) !== 'undefined',
+ accountMenuOpen: accountMenuOpen(state),
fileMenuOpen: fileMenuOpen(state),
editMenuOpen: editMenuOpen(state),
- languageMenuOpen: languageMenuOpen(state)
+ isRtl: state.locales.isRtl,
+ languageMenuOpen: languageMenuOpen(state),
+ loginMenuOpen: loginMenuOpen(state),
+ sessionExists: state.session && typeof state.session.session !== 'undefined',
+ username: state.session && state.session.session && state.session.session.user ?
+ state.session.session.user.username : null
});
const mapDispatchToProps = dispatch => ({
onOpenTipLibrary: () => dispatch(openTipsLibrary()),
+ onClickAccount: () => dispatch(openAccountMenu()),
+ onRequestCloseAccount: () => dispatch(closeAccountMenu()),
onClickFile: () => dispatch(openFileMenu()),
onRequestCloseFile: () => dispatch(closeFileMenu()),
onClickEdit: () => dispatch(openEditMenu()),
onRequestCloseEdit: () => dispatch(closeEditMenu()),
onClickLanguage: () => dispatch(openLanguageMenu()),
onRequestCloseLanguage: () => dispatch(closeLanguageMenu()),
+ onClickLogin: () => dispatch(openLoginMenu()),
+ onRequestCloseLogin: () => dispatch(closeLoginMenu()),
onSeeCommunity: () => dispatch(setPlayer(true))
});
diff --git a/src/components/menu-bar/project-title-input.css b/src/components/menu-bar/project-title-input.css
new file mode 100644
index 00000000000..0567ca76614
--- /dev/null
+++ b/src/components/menu-bar/project-title-input.css
@@ -0,0 +1,46 @@
+@import "../../css/colors.css";
+@import "../../css/units.css";
+@import "../../css/z-index.css";
+
+/*
+If project-title-input.jsx is part of a menu bar say menu-bar.jsx, it can have additional css classes that
+can set a width for example or what it should do in a flex box (eg. grow).
+*/
+
+.title-field {
+ border: 1px dashed $ui-black-transparent;
+ border-radius: $form-radius;
+ -webkit-border-radius: $form-radius;
+ -moz-border-radius: $form-radius;
+ background-color: $ui-white-transparent;
+ background-clip: padding-box;
+ -webkit-background-clip: padding-box;
+ height: auto;
+ padding: .5rem;
+}
+
+.title-field {
+ color: $ui-white;
+ font-weight: bold;
+ font-size: .8rem;
+}
+
+.title-field::placeholder {
+ color: $ui-white;
+ font-weight: normal;
+ font-size: .8rem;
+ font-style: italic;
+}
+
+.title-field:hover {
+ background-color: hsla(0, 100%, 100%, 0.5);
+}
+
+.title-field:focus {
+ outline:none;
+ border: 1px solid $ui-transparent;
+ -webkit-box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent;
+ box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent;
+ background-color: $ui-white;
+ color: $text-primary;
+}
diff --git a/src/components/menu-bar/project-title-input.jsx b/src/components/menu-bar/project-title-input.jsx
new file mode 100644
index 00000000000..b9b5aa6d838
--- /dev/null
+++ b/src/components/menu-bar/project-title-input.jsx
@@ -0,0 +1,67 @@
+import classNames from 'classnames';
+import {connect} from 'react-redux';
+import PropTypes from 'prop-types';
+import bindAll from 'lodash.bindall';
+import React from 'react';
+import {defineMessages, intlShape, injectIntl} from 'react-intl';
+
+import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
+import Input from '../forms/input.jsx';
+const BufferedInput = BufferedInputHOC(Input);
+
+import styles from './project-title-input.css';
+
+const messages = defineMessages({
+ projectTitlePlaceholder: {
+ id: 'gui.gui.projectTitlePlaceholder',
+ description: 'Placeholder for project title when blank',
+ defaultMessage: 'Project title here'
+ }
+});
+
+class ProjectTitleInput extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleUpdateProjectTitle'
+ ]);
+ }
+ // call onUpdateProjectTitle if it is defined (only defined when gui
+ // is used within scratch-www)
+ handleUpdateProjectTitle (newTitle) {
+ if (this.props.onUpdateProjectTitle) {
+ this.props.onUpdateProjectTitle(newTitle);
+ }
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+ProjectTitleInput.propTypes = {
+ className: PropTypes.string,
+ intl: intlShape.isRequired,
+ onUpdateProjectTitle: PropTypes.func,
+ projectTitle: PropTypes.string
+};
+
+const mapStateToProps = state => ({
+ projectTitle: state.scratchGui.projectTitle
+});
+
+const mapDispatchToProps = () => ({});
+
+export default injectIntl(connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ProjectTitleInput));
diff --git a/src/components/menu/menu.jsx b/src/components/menu/menu.jsx
index ce443db55cd..4e6a6b22917 100644
--- a/src/components/menu/menu.jsx
+++ b/src/components/menu/menu.jsx
@@ -32,6 +32,7 @@ MenuComponent.propTypes = {
place: PropTypes.oneOf(['left', 'right'])
};
+
const MenuItem = ({
children,
className,
@@ -51,6 +52,7 @@ MenuItem.propTypes = {
onClick: PropTypes.func
};
+
const addDividerClassToFirstChild = (child, id) => (
React.cloneElement(child, {
className: classNames(child.className, {
diff --git a/src/components/mic-indicator/mic-indicator.css b/src/components/mic-indicator/mic-indicator.css
new file mode 100644
index 00000000000..9fb3970297b
--- /dev/null
+++ b/src/components/mic-indicator/mic-indicator.css
@@ -0,0 +1,10 @@
+@keyframes popIn {
+ from {transform: scale(0.5)}
+ to {transform: scale(1)}
+}
+
+.mic-img {
+ margin: 10px;
+ transform-origin: center;
+ animation: popIn 0.1s ease-in-out;
+}
diff --git a/src/components/mic-indicator/mic-indicator.jsx b/src/components/mic-indicator/mic-indicator.jsx
new file mode 100644
index 00000000000..fe0eb602b53
--- /dev/null
+++ b/src/components/mic-indicator/mic-indicator.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './mic-indicator.css';
+import micIcon from './mic-indicator.svg';
+import {stageSizeToTransform} from '../../lib/screen-utils';
+
+const MicIndicatorComponent = props => (
+
+
+
+);
+
+MicIndicatorComponent.propTypes = {
+ className: PropTypes.string,
+ stageSize: PropTypes.shape({
+ width: PropTypes.number,
+ height: PropTypes.number,
+ widthDefault: PropTypes.number,
+ heightDefault: PropTypes.number
+ }).isRequired
+};
+
+export default MicIndicatorComponent;
diff --git a/src/components/mic-indicator/mic-indicator.svg b/src/components/mic-indicator/mic-indicator.svg
new file mode 100644
index 00000000000..78726389d13
--- /dev/null
+++ b/src/components/mic-indicator/mic-indicator.svg
@@ -0,0 +1,40 @@
+
+
+
+ mic-indicator
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css
index 9a5a035a7de..1ed2425ce1e 100644
--- a/src/components/modal/modal.css
+++ b/src/components/modal/modal.css
@@ -83,10 +83,14 @@ $sides: 20rem;
user-select: none;
}
-.header-image {
+[dir="ltr"] .header-image {
margin-right: 0.5rem;
}
+[dir="rtl"] .header-image {
+ margin-left: 0.5rem;
+}
+
.header-item-filter {
display: flex;
flex-basis: $sides;
@@ -100,13 +104,24 @@ $sides: 20rem;
user-select: none;
letter-spacing: 0.4px;
cursor: default;
+}
+
+[dir="ltr"] .header-item-title {
margin: 0 -$sides 0 0;
}
-.full-screen .header-item-title {
+[dir="rtl"] .header-item-title {
+ margin: 0 0 0 -$sides;
+}
+
+.full-screen [dir="ltr"] .header-item-title {
margin: 0 0 0 -$sides;
}
+.full-screen [dir="rtl"] .header-item-title {
+ margin: 0 -$sides 0 0;
+}
+
.header-item-close {
flex-basis: $sides;
justify-content: flex-end;
@@ -120,5 +135,36 @@ $sides: 20rem;
.back-button {
font-weight: normal;
+ padding-right: 0;
+ padding-left: 0;
+}
+
+[dir="rtl"] .back-button img {
+ transform: scaleX(-1);
+}
+
+.header-item-help {
+ padding: 0;
+ z-index: 1;
+}
+
+[dir="ltr"] .header-item-help {
+ margin-right: -4.75rem;
+}
+
+[dir="rtl"] .header-item-help {
+ margin-left: -4.75rem;
+}
+
+.help-button {
+ font-weight: normal;
+ font-size: 0.75rem;
+}
+
+[dir="ltr"] .help-button {
+ padding-right: 0;
+}
+
+[dir="rtl"] .help-button {
padding-left: 0;
}
diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx
index 940af75586f..32fe0502854 100644
--- a/src/components/modal/modal.jsx
+++ b/src/components/modal/modal.jsx
@@ -9,6 +9,7 @@ import Button from '../button/button.jsx';
import CloseButton from '../close-button/close-button.jsx';
import backIcon from '../../lib/assets/icon--back.svg';
+import helpIcon from '../../lib/assets/icon--help.svg';
import styles from './modal.css';
@@ -23,10 +24,31 @@ const ModalComponent = props => (
onRequestClose={props.onRequestClose}
>
+ {props.onHelp ? (
+
+
+
+
+
+ ) : null}
{
- const scaleX = width / widthDefault;
- const scaleY = height / heightDefault;
- if (scaleX === 1 && scaleY === 1) {
- // Do not set a transform if the scale is 1 because
- // it messes up `position: fixed` elements like the context menu.
- return;
- }
- return {transform: `scale(${scaleX},${scaleY})`};
-};
-
const MonitorList = props => (
{'+' /* TODO waiting on asset */}
@@ -35,7 +35,7 @@ const ListMonitor = ({draggable, label, width, height, value, onResizeMouseDown,
{`length ${value.length}`}
{'=' /* TODO waiting on asset */}
diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css
index d6cf6dc4568..6e17cb6ea08 100644
--- a/src/components/preview-modal/preview-modal.css
+++ b/src/components/preview-modal/preview-modal.css
@@ -74,17 +74,28 @@
color: white;
}
-.button-row button + button {
+[dir="ltr"] .button-row button + button {
margin-left: 0.5rem;
}
+[dir="rtl"] .button-row button + button {
+ margin-right: 0.5rem;
+}
+
.cat-icon {
- margin-left: .125rem;
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;
diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx
index 5772b518539..babfc0a275c 100644
--- a/src/components/preview-modal/preview-modal.jsx
+++ b/src/components/preview-modal/preview-modal.jsx
@@ -12,6 +12,26 @@ const messages = defineMessages({
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'
}
});
@@ -23,93 +43,99 @@ const PreviewModal = ({intl, ...props}) => (
overlayClassName={styles.modalOverlay}
onRequestClose={props.onTryIt}
>
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ { /* eslint-disable max-len */ }
-
-
+ { /* eslint-enable max-len */ }
+
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+
+
+
+ previewFaqLink: (
+
+
+
)
}}
/>
-
-
-
-
-
-
-
-
-
- )
- }}
- />
+
-
+
);
PreviewModal.propTypes = {
intl: intlShape.isRequired,
+ isRtl: PropTypes.bool,
onCancel: PropTypes.func.isRequired,
onTryIt: PropTypes.func.isRequired,
onViewProject: PropTypes.func.isRequired
diff --git a/src/components/preview-modal/welcome.png b/src/components/preview-modal/welcome.png
index 520b619919b..1172df932b4 100644
Binary files a/src/components/preview-modal/welcome.png and b/src/components/preview-modal/welcome.png differ
diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css
index 78e3ecb27c6..911271f1997 100644
--- a/src/components/prompt/prompt.css
+++ b/src/components/prompt/prompt.css
@@ -60,10 +60,14 @@
color: white;
}
-.button-row button + button {
+[dir="ltr"] .button-row button + button {
margin-left: 0.5rem;
}
+[dir="rtl"] .button-row button + button {
+ margin-right: 0.5rem;
+}
+
.more-options {
border-top: 1px dashed hsla(0, 0%, 0%, .25);
overflow: visible;
@@ -84,8 +88,15 @@
.more-options-icon {
width: .75rem;
height: .75rem;
- margin-left: .5rem;
vertical-align: middle;
padding-bottom: .2rem;
opacity: .5;
}
+
+[dir="ltr"] .more-options-icon {
+ margin-left: .5rem;
+}
+
+[dir="rtl"] .more-options-icon {
+ margin-right: .5rem;
+}
diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx
index 3e5ec7950fc..78192f3adaa 100644
--- a/src/components/prompt/prompt.jsx
+++ b/src/components/prompt/prompt.jsx
@@ -4,7 +4,7 @@ import React from 'react';
import Box from '../box/box.jsx';
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
-import Modal from '../modal/modal.jsx';
+import Modal from '../../containers/modal.jsx';
import styles from './prompt.css';
@@ -48,6 +48,7 @@ const PromptComponent = props => (
+
+
+ General/Check
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/question/question.css b/src/components/question/question.css
index ad5e748e67f..a0f8116ed1a 100644
--- a/src/components/question/question.css
+++ b/src/components/question/question.css
@@ -1,13 +1,6 @@
@import "../../css/units.css";
@import "../../css/colors.css";
-.question-wrapper {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
-}
-
.question-container {
margin: $space;
border: 1px solid $ui-black-transparent;
@@ -33,7 +26,6 @@
.question-submit-button {
position: absolute;
top: calc($space / 2);
- right: calc($space / 2);
width: calc(2rem - $space);
height: calc(2rem - $space);
@@ -45,13 +37,36 @@
background: $motion-primary;
}
+[dir="ltr"] .question-submit-button {
+ right: calc($space / 2);
+}
+
+[dir="rtl"] .question-submit-button {
+ left: calc($space / 2);
+}
+
/* Input overrides: width, font-weight, focus outline and padding */
.question-input > input {
width: 100%;
- padding: 0 2rem 0 0.75rem; /* To make room for the submit button */
font-weight: normal;
}
+[dir="ltr"] .question-input > input {
+ padding: 0 2rem 0 .75rem; /* To make room for the submit button */
+}
+
+[dir="rtl"] .question-input > input {
+ padding: 0 .75rem 0 2rem; /* To make room for the submit button */
+}
+
.question-input > input:focus {
box-shadow: 0px 0px 0px 3px $motion-transparent;
}
+
+.question-submit-button-icon {
+ width: calc(2rem - $space);
+ height: calc(2rem - $space);
+ position: relative;
+ right: -7px;
+ left: -7px;
+}
diff --git a/src/components/question/question.jsx b/src/components/question/question.jsx
index 2e8b9c9a531..373f7cb919c 100644
--- a/src/components/question/question.jsx
+++ b/src/components/question/question.jsx
@@ -2,17 +2,19 @@ import PropTypes from 'prop-types';
import React from 'react';
import styles from './question.css';
import Input from '../forms/input.jsx';
+import enterIcon from './icon--enter.svg';
const QuestionComponent = props => {
const {
answer,
+ className,
question,
onChange,
onClick,
onKeyPress
} = props;
return (
-
+
{question ? (
{question}
@@ -28,7 +30,11 @@ const QuestionComponent = props => {
className={styles.questionSubmitButton}
onClick={onClick}
>
- {'✔︎' /* @todo should this be an image? */}
+
@@ -38,6 +44,7 @@ const QuestionComponent = props => {
QuestionComponent.propTypes = {
answer: PropTypes.string,
+ className: PropTypes.string,
onChange: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
diff --git a/src/components/record-modal/playback-step.jsx b/src/components/record-modal/playback-step.jsx
index 40167172dea..8f76b506f33 100644
--- a/src/components/record-modal/playback-step.jsx
+++ b/src/components/record-modal/playback-step.jsx
@@ -87,7 +87,7 @@ const PlaybackStep = props => (
.input-group {
+ padding-left: 0;
padding-right: calc(2 * $space);
+ border-left: none;
border-right: 1px dashed $ui-black-transparent;
}
@@ -87,26 +107,31 @@ $border-radius: 0.25rem;
/*min-width: 1.5rem;*/
}
+[dir="rtl"] .undo-icon, [dir="rtl"] .redo-icon {
+ transform: scaleX(-1);
+}
+
.trim-button {
display: flex;
align-items: center;
color: $text-primary;
font-size: 0.625rem;
- margin-left: 1rem;
user-select: none;
}
+[dir="ltr"] .trim-button {
+ margin-left: 1rem;
+}
+
+[dir="rtl"] .trim-button {
+ margin-right: 1rem;
+}
+
.trim-button > img {
width: 1.25rem;
margin-bottom: -0.375rem;
}
-.input-group-right {
- flex-grow: 1;
- display: flex;
- flex-direction: row-reverse;
-}
-
.effect-button {
flex-basis: 60px;
color: $text-primary;
@@ -124,26 +149,56 @@ $border-radius: 0.25rem;
margin-bottom: -0.375rem;
}
-.button-group {
+/* mirror the louder/softer speaker icons when rtl */
+[dir="rtl"] .effect-button:nth-of-type(6) img {
+ transform: scaleX(-1);
+}
+
+[dir="rtl"] .effect-button:nth-of-type(7) img {
+ transform: scaleX(-1);
+}
+
+[dir="ltr"] .button-group {
margin-left: 1rem;
}
+[dir="rtl"] .button-group {
+ margin-right: 1rem;
+}
+
.button-group .button {
border-radius: 0;
+}
+
+[dir="ltr"] .button-group .button {
border-left: none;
}
+[dir="rtl"] .button-group .button {
+ border-right: none;
+}
-.button-group .button:last-of-type {
+[dir="ltr"] .button-group .button:last-of-type {
border-top-right-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
-.button-group .button:first-of-type {
+[dir="ltr"] .button-group .button:first-of-type {
border-left: 1px solid $ui-black-transparent;
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
+[dir="rtl"] .button-group .button:last-of-type {
+ border-top-left-radius: $border-radius;
+ border-bottom-left-radius: $border-radius;
+}
+
+[dir="rtl"] .button-group .button:first-of-type {
+ border-right: 1px solid $ui-black-transparent;
+ border-top-right-radius: $border-radius;
+ border-bottom-right-radius: $border-radius;
+}
+
.button:disabled > img {
opacity: 0.25;
}
diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx
index a4aafcd9155..f97338e06a5 100644
--- a/src/components/sound-editor/sound-editor.jsx
+++ b/src/components/sound-editor/sound-editor.jsx
@@ -122,6 +122,7 @@ const SoundEditor = props => (
onClick={props.onUndo}
>
@@ -133,6 +134,7 @@ const SoundEditor = props => (
onClick={props.onRedo}
>
@@ -168,7 +170,7 @@ const SoundEditor = props => (
/>
-
+
{props.playhead ? (
);
- const directionLabel = (
-
- );
+
+ const labelAbove = isWideLocale(this.props.intl.locale);
const spriteNameInput = (
-
+
{spriteNameInput}
@@ -167,7 +175,7 @@ class SpriteInfo extends React.Component {
{yPosition}
-
+
{
stageSize === STAGE_DISPLAY_SIZES.large ?
-
-
-
+
@@ -265,6 +268,7 @@ SpriteInfo.propTypes = {
name: PropTypes.string,
onChangeDirection: PropTypes.func,
onChangeName: PropTypes.func,
+ onChangeRotationStyle: PropTypes.func,
onChangeSize: PropTypes.func,
onChangeX: PropTypes.func,
onChangeY: PropTypes.func,
@@ -272,6 +276,7 @@ SpriteInfo.propTypes = {
onClickVisible: PropTypes.func,
onPressNotVisible: PropTypes.func,
onPressVisible: PropTypes.func,
+ rotationStyle: PropTypes.string,
size: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css
index 32a5339a75c..d29d15c82c0 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.css
+++ b/src/components/sprite-selector-item/sprite-selector-item.css
@@ -42,6 +42,8 @@
.sprite-image {
margin: auto;
user-select: none;
+ max-width: 32px;
+ max-height: 32px;
}
.sprite-info {
@@ -78,15 +80,29 @@
.delete-button {
position: absolute;
top: 0.125rem;
- right: 0.125rem;
z-index: 1;
}
+[dir="ltr"] .delete-button {
+ right: 0.125rem;
+}
+
+[dir="rtl"] .delete-button {
+ left: 0.125rem;
+}
+
.number {
position: absolute;
top: 0.15rem;
- left: 0.15rem;
font-size: 0.625rem;
font-weight: bold;
z-index: 2;
}
+
+[dir="ltr"] .number {
+ left: 0.15rem;
+}
+
+[dir="rtl"] .number {
+ right: 0.15rem;
+}
diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx
index cb04c034ad6..50554f94060 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.jsx
+++ b/src/components/sprite-selector-item/sprite-selector-item.jsx
@@ -2,7 +2,6 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
-import CostumeCanvas from '../costume-canvas/costume-canvas.jsx';
import CloseButton from '../close-button/close-button.jsx';
import styles from './sprite-selector-item.css';
import {ContextMenuTrigger} from 'react-contextmenu';
@@ -38,11 +37,9 @@ const SpriteSelectorItem = props => (
{props.number}
)}
{props.costumeURL ? (
-
) : null}
diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx
index a29912b7938..df42c6c28e9 100644
--- a/src/components/sprite-selector/sprite-list.jsx
+++ b/src/components/sprite-selector/sprite-list.jsx
@@ -53,8 +53,11 @@ const SpriteList = function (props) {
// Note the absence of the self-sharing check: a sprite can share assets with itself.
// This is a quirk of 2.0, but seems worth leaving possible, it
// allows quick (albeit unusual) duplication of assets.
- isRaised = isRaised || draggingType === DragConstants.COSTUME ||
- draggingType === DragConstants.SOUND;
+ isRaised = isRaised || [
+ DragConstants.COSTUME,
+ DragConstants.SOUND,
+ DragConstants.BACKPACK_COSTUME,
+ DragConstants.BACKPACK_SOUND].includes(draggingType);
return (
.sprite {
background: black;
- filter: opacity(15%) brightness(20%);
+ filter: opacity(15%) brightness(0%);
}
diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx
index da09bf9ca00..ff594d0f118 100644
--- a/src/components/sprite-selector/sprite-selector.jsx
+++ b/src/components/sprite-selector/sprite-selector.jsx
@@ -2,12 +2,12 @@ import PropTypes from 'prop-types';
import React from 'react';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
-
import Box from '../box/box.jsx';
import SpriteInfo from '../../containers/sprite-info.jsx';
import SpriteList from './sprite-list.jsx';
import ActionMenu from '../action-menu/action-menu.jsx';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants';
+import {rtlLocales} from '../../lib/locale-utils';
import styles from './sprite-selector.css';
@@ -36,7 +36,7 @@ const messages = defineMessages({
addSpriteFromFile: {
id: 'gui.spriteSelector.addSpriteFromFile',
description: 'Button to add a sprite in the target pane from file',
- defaultMessage: 'Upload'
+ defaultMessage: 'Upload Sprite'
}
});
@@ -47,6 +47,7 @@ const SpriteSelectorComponent = function (props) {
intl,
onChangeSpriteDirection,
onChangeSpriteName,
+ onChangeSpriteRotationStyle,
onChangeSpriteSize,
onChangeSpriteVisibility,
onChangeSpriteX,
@@ -84,6 +85,7 @@ const SpriteSelectorComponent = function (props) {
direction={selectedSprite.direction}
disabled={spriteInfoDisabled}
name={selectedSprite.name}
+ rotationStyle={selectedSprite.rotationStyle}
size={selectedSprite.size}
stageSize={stageSize}
visible={selectedSprite.visible}
@@ -91,6 +93,7 @@ const SpriteSelectorComponent = function (props) {
y={selectedSprite.y}
onChangeDirection={onChangeSpriteDirection}
onChangeName={onChangeSpriteName}
+ onChangeRotationStyle={onChangeSpriteRotationStyle}
onChangeSize={onChangeSpriteSize}
onChangeVisibility={onChangeSpriteVisibility}
onChangeX={onChangeSpriteX}
@@ -137,6 +140,7 @@ const SpriteSelectorComponent = function (props) {
}
]}
title={intl.formatMessage(messages.addSpriteFromLibrary)}
+ tooltipPlace={rtlLocales.indexOf(intl.locale) === -1 ? 'left' : 'right'}
onClick={onNewSpriteClick}
/>
@@ -152,6 +156,7 @@ SpriteSelectorComponent.propTypes = {
intl: intlShape.isRequired,
onChangeSpriteDirection: PropTypes.func,
onChangeSpriteName: PropTypes.func,
+ onChangeSpriteRotationStyle: PropTypes.func,
onChangeSpriteSize: PropTypes.func,
onChangeSpriteVisibility: PropTypes.func,
onChangeSpriteX: PropTypes.func,
diff --git a/src/components/stage-header/stage-header.css b/src/components/stage-header/stage-header.css
index 93ef5b4408a..3e6f59c75ac 100644
--- a/src/components/stage-header/stage-header.css
+++ b/src/components/stage-header/stage-header.css
@@ -22,7 +22,8 @@
flex-shrink: 0;
align-items: center;
height: $stage-menu-height;
- padding: $space;
+ padding-top: $space;
+ padding-bottom: $space;
}
.stage-size-row {
@@ -31,9 +32,16 @@
.stage-size-toggle-group {
display: flex;
+}
+
+[dir="ltr"] .stage-size-toggle-group {
margin-right: .2rem;
}
+[dir="rtl"] .stage-size-toggle-group {
+ margin-left: .2rem;
+}
+
.stage-button {
display: block;
border: 1px solid $ui-black-transparent;
@@ -51,13 +59,28 @@
height: 100%;
}
-.stage-button-right {
+[dir="rtl"] .stage-button-icon {
+ transform: scaleX(-1);
+}
+
+[dir="ltr"] .stage-button-first {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+[dir="ltr"] .stage-button-last {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
-.stage-button-left {
+[dir="rtl"] .stage-button-first {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+[dir="rtl"] .stage-button-last {
+ border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
diff --git a/src/components/stage-header/stage-header.jsx b/src/components/stage-header/stage-header.jsx
index 21a156a95a6..43414179b7b 100644
--- a/src/components/stage-header/stage-header.jsx
+++ b/src/components/stage-header/stage-header.jsx
@@ -96,7 +96,7 @@ const StageHeaderComponent = function (props) {
{
const {
backdropCount,
+ containerRef,
+ dragOver,
fileInputRef,
intl,
selected,
@@ -60,9 +61,10 @@ const StageSelector = props => {
{
{url ? (
-
) : null}
@@ -128,6 +128,8 @@ const StageSelector = props => {
StageSelector.propTypes = {
backdropCount: PropTypes.number.isRequired,
+ containerRef: PropTypes.func,
+ dragOver: PropTypes.bool,
fileInputRef: PropTypes.func,
intl: intlShape.isRequired,
onBackdropFileUpload: PropTypes.func,
diff --git a/src/components/stage-wrapper/stage-wrapper.css b/src/components/stage-wrapper/stage-wrapper.css
index 29ca66aa292..9f6ab5c6149 100644
--- a/src/components/stage-wrapper/stage-wrapper.css
+++ b/src/components/stage-wrapper/stage-wrapper.css
@@ -6,9 +6,6 @@
}
.stage-canvas-wrapper {
- padding-left: $space;
- padding-right: $space;
-
/* Hides negative space between edge of rounded corners + container, when selected */
user-select: none;
}
diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css
index 003947533ca..98a0c42a715 100644
--- a/src/components/stage/stage.css
+++ b/src/components/stage/stage.css
@@ -11,7 +11,7 @@
/* Attach border radius directly to canvas to prevent needing overflow:hidden; */
border-radius: $space;
- border: 1px solid $ui-black-transparent;
+ border: $stage-standard-border-width solid $ui-black-transparent;
/* @todo: This is for overriding the value being set somewhere. Where is it being set? */
background-color: transparent;
@@ -51,15 +51,17 @@
bottom: 0;
z-index: $z-index-stage-wrapper-overlay;
background-color: $ui-white;
+ /* spacing between stage and control bar (on the top), or between
+ stage and window edges (on left/right/bottom) */
+ padding: $stage-full-screen-stage-padding;
}
+/* wraps only main content of overlay player, not monitors */
.stage-overlay-content {
outline: none;
margin: auto;
- border: 3px solid rgb(126, 133, 151);
+ border: $stage-full-screen-border-width solid rgb(126, 133, 151);
padding: 0;
- margin-top: 3px;
- margin-bottom: 3px;
border-radius: $space;
overflow: hidden;
@@ -72,11 +74,22 @@
border: none;
}
-.question-wrapper {
- position: absolute;
+/* adjust monitors when stage is standard size:
+shift them down and right to compensate for the stage's border */
+.stage-wrapper .monitor-wrapper {
+ top: $stage-standard-border-width;
+ left: $stage-standard-border-width;
}
-.monitor-wrapper, .color-picker-wrapper, .queston-wrapper {
+/* adjust monitors when stage is full screen:
+.stage-wrapper-overlay uses position: fixed instead of relative, so we need
+to adjust for the border using a different method */
+.stage-wrapper-overlay .monitor-wrapper {
+ padding-top: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
+ padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
+}
+
+.monitor-wrapper, .color-picker-wrapper {
position: absolute;
top: 0;
left: 0;
@@ -93,3 +106,24 @@
z-index: $z-index-dragging-sprite;
filter: drop-shadow(5px 5px 5px $ui-black-transparent);
}
+
+.stage-bottom-wrapper {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ top: 0;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.mic-indicator {
+ transform-origin: bottom right;
+ z-index: $z-index-stage-indicator;
+ pointer-events: none;
+ align-self: flex-end;
+}
+
+.question-wrapper {
+ pointer-events: auto;
+}
diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx
index d8c0d07749b..9e1f3454dd1 100644
--- a/src/components/stage/stage.jsx
+++ b/src/components/stage/stage.jsx
@@ -7,6 +7,7 @@ import DOMElementRenderer from '../../containers/dom-element-renderer.jsx';
import Loupe from '../loupe/loupe.jsx';
import MonitorList from '../../containers/monitor-list.jsx';
import Question from '../../containers/question.jsx';
+import MicIndicator from '../mic-indicator/mic-indicator.jsx';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
import {getStageDimensions} from '../../lib/screen-utils.js';
import styles from './stage.css';
@@ -18,6 +19,7 @@ const StageComponent = props => {
isColorPicking,
isFullScreen,
colorInfo,
+ micIndicator,
question,
stageSize,
useEditorDragStyle,
@@ -66,13 +68,22 @@ const StageComponent = props => {
) : null}
- {question === null ? null : (
-
+
+ {micIndicator ? (
+
+ ) : null}
+ {question === null ? null : (
{
onQuestionAnswered={onQuestionAnswered}
/>
-
- )}
+ )}
+
(
- {title}
+
);
TagButtonComponent.propTypes = {
...Button.propTypes,
active: PropTypes.bool,
- title: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.object // FormattedMessage
- ]).isRequired
+ intlLabel: PropTypes.shape({
+ defaultMessage: PropTypes.string,
+ description: PropTypes.string,
+ id: PropTypes.string
+ }).isRequired,
+ tag: PropTypes.string.isRequired
};
TagButtonComponent.defaultProps = {
diff --git a/src/components/target-pane/target-pane.css b/src/components/target-pane/target-pane.css
index 404283d19fd..07b07cb161a 100644
--- a/src/components/target-pane/target-pane.css
+++ b/src/components/target-pane/target-pane.css
@@ -11,5 +11,12 @@
display: flex;
flex-basis: 72px;
flex-shrink: 0;
+}
+
+[dir="ltr"] .stage-selector-wrapper {
margin-left: calc($space / 2);
}
+
+[dir="rtl"] .stage-selector-wrapper {
+ margin-right: calc($space / 2);
+}
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index c620c8504c1..565c9881cf4 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -23,6 +23,7 @@ const TargetPane = ({
spriteLibraryVisible,
onChangeSpriteDirection,
onChangeSpriteName,
+ onChangeSpriteRotationStyle,
onChangeSpriteSize,
onChangeSpriteVisibility,
onChangeSpriteX,
@@ -60,6 +61,7 @@ const TargetPane = ({
stageSize={stageSize}
onChangeSpriteDirection={onChangeSpriteDirection}
onChangeSpriteName={onChangeSpriteName}
+ onChangeSpriteRotationStyle={onChangeSpriteRotationStyle}
onChangeSpriteSize={onChangeSpriteSize}
onChangeSpriteVisibility={onChangeSpriteVisibility}
onChangeSpriteX={onChangeSpriteX}
@@ -128,6 +130,7 @@ TargetPane.propTypes = {
}),
onChangeSpriteDirection: PropTypes.func,
onChangeSpriteName: PropTypes.func,
+ onChangeSpriteRotationStyle: PropTypes.func,
onChangeSpriteSize: PropTypes.func,
onChangeSpriteVisibility: PropTypes.func,
onChangeSpriteX: PropTypes.func,
diff --git a/src/components/webgl-modal/webgl-modal.css b/src/components/webgl-modal/webgl-modal.css
index f1b6ff6efee..10eed71f1d4 100644
--- a/src/components/webgl-modal/webgl-modal.css
+++ b/src/components/webgl-modal/webgl-modal.css
@@ -34,6 +34,10 @@
background-size: cover;
}
+[dir="rtl"] .illustration {
+ transform: scaleX(-1);
+}
+
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
diff --git a/src/components/webgl-modal/webgl-modal.jsx b/src/components/webgl-modal/webgl-modal.jsx
index 0a7b753aae4..faf5ac55171 100644
--- a/src/components/webgl-modal/webgl-modal.jsx
+++ b/src/components/webgl-modal/webgl-modal.jsx
@@ -22,76 +22,79 @@ const WebGlModal = ({intl, ...props}) => (
overlayClassName={styles.modalOverlay}
onRequestClose={props.onBack}
>
-
+
+
-
-
-
-
-
- { /* eslint-disable max-len */ }
-
-
-
- )
- }}
- />
- { /* eslint-enable max-len */ }
-
-
-
-
+
+
+
+
+
+ { /* eslint-disable max-len */ }
+
+
+ )
+ }}
/>
-
+ { /* eslint-enable max-len */ }
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
-
-
-
-
- )
- }}
- />
-
-
+
);
WebGlModal.propTypes = {
intl: intlShape.isRequired,
+ isRtl: PropTypes.bool,
onBack: PropTypes.func.isRequired
};
diff --git a/src/containers/account-nav.jsx b/src/containers/account-nav.jsx
new file mode 100644
index 00000000000..d0ce3b0628b
--- /dev/null
+++ b/src/containers/account-nav.jsx
@@ -0,0 +1,53 @@
+/*
+NOTE: this file only temporarily resides in scratch-gui.
+Nearly identical code appears in scratch-www, and the two should
+eventually be consolidated.
+*/
+
+import {injectIntl} from 'react-intl';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {connect} from 'react-redux';
+
+import AccountNavComponent from '../components/menu-bar/account-nav.jsx';
+
+const AccountNav = function (props) {
+ const {
+ ...componentProps
+ } = props;
+ return (
+
+ );
+};
+
+AccountNav.propTypes = {
+ classroomId: PropTypes.string,
+ isEducator: PropTypes.bool,
+ isRtl: PropTypes.bool,
+ isStudent: PropTypes.bool,
+ profileUrl: PropTypes.string,
+ thumbnailUrl: PropTypes.string,
+ username: PropTypes.string
+};
+
+const mapStateToProps = state => ({
+ classroomId: state.session && state.session.session && state.session.session.user ?
+ state.session.session.user.classroomId : '',
+ isEducator: state.session && state.session.permissions && state.session.permissions.educator,
+ isStudent: state.session && state.session.permissions && state.session.permissions.student,
+ profileUrl: state.session && state.session.session && state.session.session.user ?
+ `/users/${state.session.session.user.username}` : '',
+ thumbnailUrl: state.session && state.session.session && state.session.session.user ?
+ state.session.session.user.thumbnailUrl : null,
+ username: state.session && state.session.session && state.session.session.user ?
+ state.session.session.user.username : ''
+});
+
+const mapDispatchToProps = () => ({});
+
+export default injectIntl(connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(AccountNav));
diff --git a/src/containers/alert.jsx b/src/containers/alert.jsx
new file mode 100644
index 00000000000..cb38427ad75
--- /dev/null
+++ b/src/containers/alert.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+
+import AlertComponent from '../components/alerts/alert.jsx';
+
+class Alert extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleOnCloseAlert'
+ ]);
+ }
+ handleOnCloseAlert () {
+ this.props.onCloseAlert(this.props.index);
+ }
+ render () {
+ const {
+ index, // eslint-disable-line no-unused-vars
+ iconURL,
+ message
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
+Alert.propTypes = {
+ iconURL: PropTypes.string,
+ index: PropTypes.number,
+ message: PropTypes.string,
+ onCloseAlert: PropTypes.func.isRequired
+};
+
+export default Alert;
diff --git a/src/containers/alerts.jsx b/src/containers/alerts.jsx
new file mode 100644
index 00000000000..21a17700a8a
--- /dev/null
+++ b/src/containers/alerts.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
+
+import {
+ closeAlert
+} from '../reducers/alerts';
+
+import AlertsComponent from '../components/alerts/alerts.jsx';
+
+const Alerts = ({
+ alertsList,
+ className,
+ onCloseAlert
+}) => (
+
+);
+
+Alerts.propTypes = {
+ alertsList: PropTypes.arrayOf(PropTypes.object),
+ className: PropTypes.string,
+ onCloseAlert: PropTypes.func
+};
+
+const mapStateToProps = state => ({
+ alertsList: state.scratchGui.alerts.alertsList
+});
+
+const mapDispatchToProps = dispatch => ({
+ onCloseAlert: index => dispatch(closeAlert(index))
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(Alerts);
diff --git a/src/containers/auto-scanning-step.jsx b/src/containers/auto-scanning-step.jsx
new file mode 100644
index 00000000000..f674738419d
--- /dev/null
+++ b/src/containers/auto-scanning-step.jsx
@@ -0,0 +1,87 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import bindAll from 'lodash.bindall';
+import ScanningStepComponent, {PHASES} from '../components/connection-modal/auto-scanning-step.jsx';
+import VM from 'scratch-vm';
+
+class AutoScanningStep extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handlePeripheralListUpdate',
+ 'handlePeripheralScanTimeout',
+ 'handleStartScan',
+ 'handleRefresh'
+ ]);
+ this.state = {
+ phase: PHASES.prescan
+ };
+ }
+ componentWillUnmount () {
+ // @todo: stop the peripheral scan here
+ this.unbindPeripheralUpdates();
+ }
+ handlePeripheralScanTimeout () {
+ this.setState({
+ phase: PHASES.notfound
+ });
+ this.unbindPeripheralUpdates();
+ }
+ handlePeripheralListUpdate (newList) {
+ // TODO: sort peripherals by signal strength? so they don't jump around
+ const peripheralArray = Object.keys(newList).map(id =>
+ newList[id]
+ );
+ if (peripheralArray.length > 0) {
+ this.props.onConnecting(peripheralArray[0].peripheralId);
+ }
+ }
+ bindPeripheralUpdates () {
+ this.props.vm.on(
+ 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate);
+ this.props.vm.on(
+ 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout);
+ }
+ unbindPeripheralUpdates () {
+ this.props.vm.removeListener(
+ 'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate);
+ this.props.vm.removeListener(
+ 'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout);
+ }
+ handleRefresh () {
+ // @todo: stop the peripheral scan here, it is more important for auto scan
+ // due to timeout and cancellation
+ this.setState({
+ phase: PHASES.prescan
+ });
+ this.unbindPeripheralUpdates();
+ }
+ handleStartScan () {
+ this.bindPeripheralUpdates();
+ this.props.vm.scanForPeripheral(this.props.extensionId);
+ this.setState({
+ phase: PHASES.pressbutton
+ });
+
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+AutoScanningStep.propTypes = {
+ extensionId: PropTypes.string.isRequired,
+ onConnecting: PropTypes.func.isRequired,
+ peripheralButtonImage: PropTypes.string,
+ vm: PropTypes.instanceOf(VM).isRequired
+};
+
+export default AutoScanningStep;
diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx
index aedf2f35da1..3f9b2dd5966 100644
--- a/src/containers/backpack.jsx
+++ b/src/containers/backpack.jsx
@@ -11,11 +11,15 @@ import {
spritePayload
} from '../lib/backpack-api';
import DragConstants from '../lib/drag-constants';
+import DropAreaHOC from '../lib/drop-area-hoc.jsx';
import {connect} from 'react-redux';
import storage from '../lib/storage';
import VM from 'scratch-vm';
+const dragTypes = [DragConstants.COSTUME, DragConstants.SOUND, DragConstants.SPRITE];
+const DroppableBackpack = DropAreaHOC(dragTypes)(BackpackComponent);
+
class Backpack extends React.Component {
constructor (props) {
super(props);
@@ -23,8 +27,8 @@ class Backpack extends React.Component {
'handleDrop',
'handleToggle',
'handleDelete',
- 'refreshContents',
- 'setRef'
+ 'getBackpackAssetURL',
+ 'refreshContents'
]);
this.state = {
dragOver: false,
@@ -41,33 +45,13 @@ class Backpack extends React.Component {
if (props.host && !storage._hasAddedBackpackSource) {
storage.addWebSource(
[storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound],
- asset => `${props.host}/${asset.assetId}.${asset.dataFormat}`
+ this.getBackpackAssetURL
);
storage._hasAddedBackpackSource = true;
}
}
- componentWillReceiveProps (newProps) {
- 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();
- // If `dragging` becomes false, call the drop handler
- } else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging && this.state.dragOver) {
- this.handleDrop(this.props.dragInfo);
- this.setState({dragOver: false});
- }
-
- // If a drag is in progress (currentOffset) and it matches the relevant drag types,
- // test if the drag is within the drop area rect and set the state accordingly.
- if (this.dropAreaRect && newProps.dragInfo.currentOffset && dragTypes.includes(newProps.dragInfo.dragType)) {
- const {x, y} = newProps.dragInfo.currentOffset;
- const {top, right, bottom, left} = this.dropAreaRect;
- if (x > left && x < right && y > top && y < bottom) {
- this.setState({dragOver: true});
- } else {
- this.setState({dragOver: false});
- }
- }
+ getBackpackAssetURL (asset) {
+ return `${this.props.host}/${asset.assetId}.${asset.dataFormat}`;
}
handleToggle () {
const newState = !this.state.expanded;
@@ -126,19 +110,15 @@ class Backpack extends React.Component {
});
}
}
- setRef (ref) {
- this.ref = ref;
- }
render () {
return (
-
);
@@ -146,15 +126,6 @@ class Backpack extends React.Component {
}
Backpack.propTypes = {
- dragInfo: PropTypes.shape({
- currentOffset: PropTypes.shape({
- x: PropTypes.number,
- y: PropTypes.number
- }),
- dragType: PropTypes.string,
- dragging: PropTypes.bool,
- index: PropTypes.number
- }),
host: PropTypes.string,
token: PropTypes.string,
username: PropTypes.string,
@@ -163,10 +134,10 @@ Backpack.propTypes = {
const getTokenAndUsername = state => {
// Look for the session state provided by scratch-www
- if (state.session && state.session.session) {
+ if (state.session && state.session.session && state.session.session.user) {
return {
- token: state.session.session.token,
- username: state.session.session.username
+ token: state.session.session.user.token,
+ username: state.session.session.user.username
};
}
// Otherwise try to pull testing params out of the URL, or return nulls
@@ -181,7 +152,6 @@ const getTokenAndUsername = state => {
const mapStateToProps = state => Object.assign(
{
- dragInfo: state.scratchGui.assetDrag,
vm: state.scratchGui.vm
},
getTokenAndUsername(state)
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index 1de665e3342..bf759bb1b89 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -8,6 +8,7 @@ import VMScratchBlocks from '../lib/blocks';
import VM from 'scratch-vm';
import analytics from '../lib/analytics';
+import log from '../lib/log.js';
import Prompt from './prompt.jsx';
import ConnectionModal from './connection-modal.jsx';
import BlocksComponent from '../components/blocks/blocks.jsx';
@@ -77,7 +78,7 @@ class Blocks extends React.Component {
const workspaceConfig = defaultsDeep({},
Blocks.defaultOptions,
this.props.options,
- {toolbox: this.props.toolboxXML}
+ {rtl: this.props.isRtl, toolbox: this.props.toolboxXML}
);
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
@@ -94,7 +95,11 @@ class Blocks extends React.Component {
addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange);
this.attachVM();
- this.setLocale();
+ // Only update blocks/vm locale when visible to avoid sizing issues
+ // If locale changes while not visible it will get handled in didUpdate
+ if (this.props.isVisible) {
+ this.setLocale();
+ }
analytics.pageview('/editors/blocks');
}
@@ -117,10 +122,6 @@ class Blocks extends React.Component {
this.ScratchBlocks.hideChaff();
}
- if (prevProps.locale !== this.props.locale) {
- this.setLocale();
- }
-
if (prevProps.toolboxXML !== this.props.toolboxXML) {
// rather than update the toolbox "sync" -- update it in the next frame
clearTimeout(this.toolboxUpdateTimeout);
@@ -139,9 +140,15 @@ class Blocks extends React.Component {
// @todo hack to reload the workspace due to gui bug #413
if (this.props.isVisible) { // Scripts tab
this.workspace.setVisible(true);
- this.props.vm.refreshWorkspace();
- // Re-enable toolbox refreshes without causing one. See #updateToolbox for more info.
- this.workspace.toolboxRefreshEnabled_ = true;
+ if (prevProps.locale !== this.props.locale || this.props.locale !== this.props.vm.getLocale()) {
+ // call setLocale if the locale has changed, or changed while the blocks were hidden.
+ // vm.getLocale() will be out of sync if locale was changed while not visible
+ this.setLocale();
+ } else {
+ this.props.vm.refreshWorkspace();
+ this.updateToolbox();
+ }
+
window.dispatchEvent(new Event('resize'));
} else {
this.workspace.setVisible(false);
@@ -158,8 +165,8 @@ class Blocks extends React.Component {
this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
this.props.vm.setLocale(this.props.locale, this.props.messages)
.then(() => {
- this.workspace.updateToolbox(this.props.toolboxXML);
this.props.vm.refreshWorkspace();
+ this.updateToolbox();
this.workspace.getFlyout().setRecyclingEnabled(true);
});
}
@@ -294,7 +301,21 @@ class Blocks extends React.Component {
// Remove and reattach the workspace listener (but allow flyout events)
this.workspace.removeChangeListener(this.props.vm.blockListener);
const dom = this.ScratchBlocks.Xml.textToDom(data.xml);
- this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace);
+ try {
+ this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace);
+ } catch (error) {
+ // The workspace is likely incomplete. What did update should be
+ // functional.
+ //
+ // Instead of throwing the error, by logging it and continuing as
+ // normal lets the other workspace update processes complete in the
+ // gui and vm, which lets the vm run even if the workspace is
+ // incomplete. Throwing the error would keep things like setting the
+ // correct editing target from happening which can interfere with
+ // some blocks and processes in the vm.
+ error.message = `Workspace Update Error: ${error.message}`;
+ log.error(error);
+ }
this.workspace.addChangeListener(this.props.vm.blockListener);
if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
@@ -304,6 +325,11 @@ class Blocks extends React.Component {
this.workspace.scale = scale;
this.workspace.resize();
}
+
+ // Clear the undo state of the workspace since this is a
+ // fresh workspace and we don't want any changes made to another sprites
+ // workspace to be 'undone' here.
+ this.workspace.clearUndo();
}
handleExtensionAdded (blocksInfo) {
// select JSON from each block info object then reject the pseudo-blocks which don't have JSON, like separators
@@ -325,7 +351,7 @@ class Blocks extends React.Component {
}
handleCategorySelected (categoryId) {
const extension = extensionData.find(ext => ext.extensionId === categoryId);
- if (extension && extension.launchDeviceConnectionFlow) {
+ if (extension && extension.launchPeripheralConnectionFlow) {
this.handleConnectionModalStart(categoryId);
}
@@ -353,9 +379,13 @@ class Blocks extends React.Component {
if (extension) {
this.setState({connectionModal: {
extensionId: extensionId,
- deviceImage: extension.deviceImage,
- smallDeviceImage: extension.smallDeviceImage,
- name: extension.name
+ useAutoScan: extension.useAutoScan,
+ peripheralImage: extension.peripheralImage,
+ smallPeripheralImage: extension.smallPeripheralImage,
+ peripheralButtonImage: extension.peripheralButtonImage,
+ name: extension.name,
+ connectingMessage: extension.connectingMessage,
+ helpLink: extension.helpLink
}});
}
}
@@ -390,6 +420,7 @@ class Blocks extends React.Component {
options,
stageSize,
vm,
+ isRtl,
isVisible,
onActivateColorPicker,
updateToolboxState,
@@ -419,10 +450,7 @@ class Blocks extends React.Component {
) : null}
{this.state.connectionModal ? (
({
state.scratchGui.mode.isFullScreen
),
extensionLibraryVisible: state.scratchGui.modals.extensionLibrary,
+ isRtl: state.locales.isRtl,
locale: state.locales.locale,
messages: state.locales.messages,
toolboxXML: state.scratchGui.toolbox.toolboxXML,
diff --git a/src/containers/cards.jsx b/src/containers/cards.jsx
index 5dc081f7b96..2e4b92e5af0 100644
--- a/src/containers/cards.jsx
+++ b/src/containers/cards.jsx
@@ -23,6 +23,7 @@ const mapStateToProps = state => ({
step: state.scratchGui.cards.step,
x: state.scratchGui.cards.x,
y: state.scratchGui.cards.y,
+ isRtl: state.locales.isRtl,
dragging: state.scratchGui.cards.dragging
});
diff --git a/src/containers/connection-modal.jsx b/src/containers/connection-modal.jsx
index 3b1a68b3adc..164f2455bb6 100644
--- a/src/containers/connection-modal.jsx
+++ b/src/containers/connection-modal.jsx
@@ -3,6 +3,7 @@ import React from 'react';
import bindAll from 'lodash.bindall';
import ConnectionModalComponent, {PHASES} from '../components/connection-modal/connection-modal.jsx';
import VM from 'scratch-vm';
+import analytics from '../lib/analytics';
class ConnectionModal extends React.Component {
constructor (props) {
@@ -12,21 +13,17 @@ class ConnectionModal extends React.Component {
'handleConnected',
'handleConnecting',
'handleDisconnect',
- 'handleError'
+ 'handleError',
+ 'handleHelp'
]);
this.state = {
- phase: PHASES.scanning
+ phase: props.vm.getPeripheralIsConnected(props.extensionId) ?
+ PHASES.connected : PHASES.scanning
};
}
componentDidMount () {
this.props.vm.on('PERIPHERAL_CONNECTED', this.handleConnected);
this.props.vm.on('PERIPHERAL_ERROR', this.handleError);
-
- // Check if we're already connected
- if (this.props.vm.getPeripheralIsConnected(this.props.extensionId)) {
- this.handleConnected();
- }
-
}
componentWillUnmount () {
this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleConnected);
@@ -38,20 +35,25 @@ class ConnectionModal extends React.Component {
});
}
handleConnecting (peripheralId) {
- this.props.vm.connectToPeripheral(this.props.extensionId, peripheralId);
+ this.props.vm.connectPeripheral(this.props.extensionId, peripheralId);
this.setState({
phase: PHASES.connecting
});
+ analytics.event({
+ category: 'extensions',
+ action: 'connecting',
+ label: this.props.extensionId
+ });
}
handleDisconnect () {
this.props.onStatusButtonUpdate(this.props.extensionId, 'not ready');
- this.props.vm.disconnectExtensionSession(this.props.extensionId);
+ this.props.vm.disconnectPeripheral(this.props.extensionId);
this.props.onCancel();
}
handleCancel () {
- // If we're not connected to a device, close the websocket so we stop scanning.
+ // If we're not connected to a peripheral, close the websocket so we stop scanning.
if (!this.props.vm.getPeripheralIsConnected(this.props.extensionId)) {
- this.props.vm.disconnectExtensionSession(this.props.extensionId);
+ this.props.vm.disconnectPeripheral(this.props.extensionId);
}
this.props.onCancel();
}
@@ -67,6 +69,11 @@ class ConnectionModal extends React.Component {
this.setState({
phase: PHASES.error
});
+ analytics.event({
+ category: 'extensions',
+ action: 'connecting error',
+ label: this.props.extensionId
+ });
}
}
handleConnected () {
@@ -74,19 +81,32 @@ class ConnectionModal extends React.Component {
this.setState({
phase: PHASES.connected
});
+ analytics.event({
+ category: 'extensions',
+ action: 'connected',
+ label: this.props.extensionId
+ });
}
handleHelp () {
- // @todo: implement the help button
+ window.open(this.props.helpLink, '_blank');
+ analytics.event({
+ category: 'extensions',
+ action: 'help',
+ label: this.props.extensionId
+ });
}
render () {
return (
@@ -73,7 +55,16 @@ class Controls extends React.Component {
}
Controls.propTypes = {
+ projectRunning: PropTypes.bool.isRequired,
+ turbo: PropTypes.bool.isRequired,
vm: PropTypes.instanceOf(VM)
};
-export default Controls;
+const mapStateToProps = state => ({
+ projectRunning: state.scratchGui.vmStatus.running,
+ turbo: state.scratchGui.vmStatus.turbo
+});
+// no-op function to prevent dispatch prop being passed to component
+const mapDispatchToProps = () => ({});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Controls);
diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx
index 454c5ffcef7..708e84adc6e 100644
--- a/src/containers/costume-tab.jsx
+++ b/src/containers/costume-tab.jsx
@@ -11,6 +11,8 @@ import {connect} from 'react-redux';
import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
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 {
closeCameraCapture,
@@ -24,6 +26,8 @@ import {
SOUNDS_TAB_INDEX
} from '../reducers/editor-tab';
+import {setRestore} from '../reducers/restore-deletion';
+
import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg';
import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg';
import fileUploadIcon from '../components/action-menu/icon--file-upload.svg';
@@ -35,7 +39,7 @@ import searchIcon from '../components/action-menu/icon--search.svg';
import costumeLibraryContent from '../lib/libraries/costumes.json';
import backdropLibraryContent from '../lib/libraries/backdrops.json';
-const messages = defineMessages({
+let messages = defineMessages({
addLibraryBackdropMsg: {
defaultMessage: 'Choose a Backdrop',
description: 'Button to add a backdrop in the editor tab',
@@ -73,6 +77,8 @@ const messages = defineMessages({
}
});
+messages = {...messages, ...sharedMessages};
+
class CostumeTab extends React.Component {
constructor (props) {
super(props);
@@ -135,7 +141,11 @@ class CostumeTab extends React.Component {
this.setState({selectedCostumeIndex: costumeIndex});
}
handleDeleteCostume (costumeIndex) {
- this.props.vm.deleteCostume(costumeIndex);
+ const restoreCostumeFun = this.props.vm.deleteCostume(costumeIndex);
+ this.props.dispatchUpdateRestore({
+ restoreFun: restoreCostumeFun,
+ deletedItem: 'Costume'
+ });
}
handleDuplicateCostume (costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex);
@@ -144,20 +154,10 @@ class CostumeTab extends React.Component {
this.props.vm.addCostume(costume.md5, costume);
}
handleNewBlankCostume () {
- const emptyItem = costumeLibraryContent.find(item => (
- item.name === 'Empty'
- ));
- const name = this.props.vm.editingTarget.isStage ? `backdrop1` : `costume1`;
- const vmCostume = {
- name: name,
- md5: emptyItem.md5,
- rotationCenterX: emptyItem.info[0],
- rotationCenterY: emptyItem.info[1],
- bitmapResolution: emptyItem.info.length > 2 ? emptyItem.info[2] : 1,
- skinId: null
- };
-
- this.handleNewCostume(vmCostume);
+ const name = this.props.vm.editingTarget.isStage ?
+ this.props.intl.formatMessage(messages.backdrop, {index: 1}) :
+ this.props.intl.formatMessage(messages.costume, {index: 1});
+ this.handleNewCostume(emptyCostume(name));
}
handleSurpriseCostume () {
const item = costumeLibraryContent[Math.floor(Math.random() * costumeLibraryContent.length)];
@@ -195,7 +195,8 @@ class CostumeTab extends React.Component {
}
handleCameraBuffer (buffer) {
const storage = this.props.vm.runtime.storage;
- costumeUpload(buffer, 'image/png', 'costume1', storage, this.handleNewCostume);
+ const name = this.props.intl.formatMessage(messages.costume, {index: 1});
+ costumeUpload(buffer, 'image/png', name, storage, this.handleNewCostume);
}
handleFileUploadClick () {
this.fileInput.click();
@@ -232,6 +233,7 @@ class CostumeTab extends React.Component {
}
render () {
const {
+ dispatchUpdateRestore, // eslint-disable-line no-unused-vars
intl,
onNewCostumeFromCameraClick,
onNewLibraryBackdropClick,
@@ -325,6 +327,7 @@ class CostumeTab extends React.Component {
CostumeTab.propTypes = {
cameraModalVisible: PropTypes.bool,
+ dispatchUpdateRestore: PropTypes.func,
editingTarget: PropTypes.string,
intl: intlShape,
onActivateSoundsTab: PropTypes.func.isRequired,
@@ -372,6 +375,9 @@ const mapDispatchToProps = dispatch => ({
},
onRequestCloseCameraModal: () => {
dispatch(closeCameraCapture());
+ },
+ dispatchUpdateRestore: restoreState => {
+ dispatch(setRestore(restoreState));
}
});
diff --git a/src/containers/custom-procedures.jsx b/src/containers/custom-procedures.jsx
index 879800aae88..e691b0466f7 100644
--- a/src/containers/custom-procedures.jsx
+++ b/src/containers/custom-procedures.jsx
@@ -19,6 +19,7 @@ class CustomProcedures extends React.Component {
'setBlocks'
]);
this.state = {
+ rtlOffset: 0,
warp: false
};
}
@@ -32,7 +33,8 @@ class CustomProcedures extends React.Component {
this.blocks = blocksRef;
const workspaceConfig = defaultsDeep({},
CustomProcedures.defaultOptions,
- this.props.options
+ this.props.options,
+ {rtl: this.props.isRtl}
);
// @todo This is a hack to make there be no toolbox.
@@ -54,11 +56,47 @@ class CustomProcedures extends React.Component {
const metrics = this.workspace.getMetrics();
const {x, y} = this.mutationRoot.getRelativeToSurfaceXY();
const dy = (metrics.viewHeight / 2) - (this.mutationRoot.height / 2) - y;
- let dx = (metrics.viewWidth / 2) - (this.mutationRoot.width / 2) - x;
- // If the procedure declaration is wider than the view width,
- // keep the right-hand side of the procedure in view.
- if (this.mutationRoot.width > metrics.viewWidth) {
- dx = metrics.viewWidth - this.mutationRoot.width - x;
+ let dx;
+ if (this.props.isRtl) {
+ // // TODO: https://github.com/LLK/scratch-gui/issues/2838
+ // This is temporary until we can figure out what's going on width
+ // block positioning on the workspace for RTL.
+ // Workspace is always origin top-left, with x increasing to the right
+ // Calculate initial starting offset and save it, every other move
+ // has to take the original offset into account.
+ // Calculate a new left postion based on new width
+ // Convert current x position into LTR (mirror) x position (uses original offset)
+ // Use the difference between ltrX and mirrorX as the amount to move
+ const ltrX = ((metrics.viewWidth / 2) - (this.mutationRoot.width / 2) + 25);
+ const mirrorX = x - ((x - this.state.rtlOffset) * 2);
+ if (mirrorX === ltrX) {
+ return;
+ }
+ dx = mirrorX - ltrX;
+ const midPoint = metrics.viewWidth / 2;
+ if (x === 0) {
+ // if it's the first time positioning, it should always move right
+ if (this.mutationRoot.width < midPoint) {
+ dx = ltrX;
+ } else if (this.mutationRoot.width < metrics.viewWidth) {
+ dx = midPoint - ((metrics.viewWidth - this.mutationRoot.width) / 2);
+ } else {
+ dx = midPoint + (this.mutationRoot.width - metrics.viewWidth);
+ }
+ this.mutationRoot.moveBy(dx, dy);
+ this.setState({rtlOffset: this.mutationRoot.getRelativeToSurfaceXY().x});
+ return;
+ }
+ if (this.mutationRoot.width > metrics.viewWidth) {
+ dx = dx + this.mutationRoot.width - metrics.viewWidth;
+ }
+ } else {
+ dx = (metrics.viewWidth / 2) - (this.mutationRoot.width / 2) - x;
+ // If the procedure declaration is wider than the view width,
+ // keep the right-hand side of the procedure in view.
+ if (this.mutationRoot.width > metrics.viewWidth) {
+ dx = metrics.viewWidth - this.mutationRoot.width - x;
+ }
}
this.mutationRoot.moveBy(dx, dy);
});
@@ -117,6 +155,7 @@ class CustomProcedures extends React.Component {
}
CustomProcedures.propTypes = {
+ isRtl: PropTypes.bool,
mutator: PropTypes.instanceOf(Element),
onRequestClose: PropTypes.func.isRequired,
options: PropTypes.shape({
@@ -147,6 +186,7 @@ CustomProcedures.defaultProps = {
};
const mapStateToProps = state => ({
+ isRtl: state.locales.isRtl,
mutator: state.scratchGui.customProcedures.mutator
});
diff --git a/src/containers/deletion-restorer.jsx b/src/containers/deletion-restorer.jsx
new file mode 100644
index 00000000000..854836eebe4
--- /dev/null
+++ b/src/containers/deletion-restorer.jsx
@@ -0,0 +1,70 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {connect} from 'react-redux';
+import {setRestore} from '../reducers/restore-deletion';
+
+/**
+ * DeletionRestorer component passes a restoreDeletion function to its child.
+ * It expects this child to be a function with the signature
+ * function (restoreDeletion, props) {}
+ * The component can then be used to attach deletion restoring functionality
+ * to any other component:
+ *
+ * {(restoreDeletion, props) => (
+ *
+ * )}
+ */
+class DeletionRestorer extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'restoreDeletion'
+ ]);
+ }
+ restoreDeletion () {
+ if (typeof this.props.restore === 'function') {
+ this.props.restore();
+ this.props.dispatchUpdateRestore({restoreFun: null, deletedItem: ''});
+ }
+ }
+ render () {
+ const {
+ /* eslint-disable no-unused-vars */
+ children,
+ dispatchUpdateRestore,
+ /* eslint-enable no-unused-vars */
+ ...props
+ } = this.props;
+ const restorable = typeof this.props.restore === 'function';
+ return this.props.children(this.restoreDeletion, {
+ ...props,
+ restorable
+ });
+ }
+}
+
+DeletionRestorer.propTypes = {
+ children: PropTypes.func,
+ deletedItem: PropTypes.string,
+ dispatchUpdateRestore: PropTypes.func,
+ restore: PropTypes.func
+};
+
+const mapStateToProps = state => ({
+ deletedItem: state.scratchGui.restoreDeletion.deletedItem,
+ restore: state.scratchGui.restoreDeletion.restoreFun
+});
+const mapDispatchToProps = dispatch => ({
+ dispatchUpdateRestore: updatedState => {
+ dispatch(setRestore(updatedState));
+ }
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(DeletionRestorer);
diff --git a/src/containers/direction-picker.jsx b/src/containers/direction-picker.jsx
new file mode 100644
index 00000000000..dcb943568ff
--- /dev/null
+++ b/src/containers/direction-picker.jsx
@@ -0,0 +1,64 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import DirectionComponent, {RotationStyles} from '../components/direction-picker/direction-picker.jsx';
+
+class DirectionPicker extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleOpenPopover',
+ 'handleClosePopover',
+ 'handleClickLeftRight',
+ 'handleClickDontRotate',
+ 'handleClickAllAround'
+ ]);
+ this.state = {
+ popoverOpen: false
+ };
+ }
+ handleOpenPopover () {
+ this.setState({popoverOpen: true});
+ }
+ handleClosePopover () {
+ this.setState({popoverOpen: false});
+ }
+ handleClickAllAround () {
+ this.props.onChangeRotationStyle(RotationStyles.ALL_AROUND);
+ }
+ handleClickLeftRight () {
+ this.props.onChangeRotationStyle(RotationStyles.LEFT_RIGHT);
+ }
+ handleClickDontRotate () {
+ this.props.onChangeRotationStyle(RotationStyles.DONT_ROTATE);
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+DirectionPicker.propTypes = {
+ direction: PropTypes.number,
+ disabled: PropTypes.bool,
+ labelAbove: PropTypes.bool,
+ onChangeDirection: PropTypes.func,
+ onChangeRotationStyle: PropTypes.func,
+ rotationStyle: PropTypes.string
+};
+
+export default DirectionPicker;
diff --git a/src/containers/error-boundary.jsx b/src/containers/error-boundary.jsx
index 52e522d5aae..6485a41b84b 100644
--- a/src/containers/error-boundary.jsx
+++ b/src/containers/error-boundary.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
import bowser from 'bowser';
import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx';
import CrashMessageComponent from '../components/crash-message/crash-message.jsx';
@@ -57,7 +58,10 @@ class ErrorBoundary extends React.Component {
if (supportedBrowser()) {
return ;
}
- return ;
+ return ( );
}
return this.props.children;
}
@@ -65,7 +69,15 @@ class ErrorBoundary extends React.Component {
ErrorBoundary.propTypes = {
action: PropTypes.string.isRequired, // Used for defining tracking action
- children: PropTypes.node
+ children: PropTypes.node,
+ isRtl: PropTypes.bool
};
-export default ErrorBoundary;
+const mapStateToProps = state => ({
+ isRtl: state.locales.isRtl
+});
+
+// no-op function to prevent dispatch prop being passed to component
+const mapDispatchToProps = () => ({});
+
+export default connect(mapStateToProps, mapDispatchToProps)(ErrorBoundary);
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index 8db598c85fd..6ef14439ba7 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -7,6 +7,7 @@ import ReactModal from 'react-modal';
import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import {openExtensionLibrary} from '../reducers/modals';
+import {setProjectTitle} from '../reducers/project-title';
import {
activateTab,
BLOCKS_TAB_INDEX,
@@ -34,6 +35,10 @@ class GUI extends React.Component {
};
}
componentDidMount () {
+ if (this.props.projectTitle) {
+ this.props.onUpdateReduxProjectTitle(this.props.projectTitle);
+ }
+
if (this.props.vm.initialized) return;
this.audioEngine = new AudioEngine();
this.props.vm.attachAudioEngine(this.audioEngine);
@@ -65,6 +70,9 @@ class GUI extends React.Component {
});
});
}
+ if (this.props.projectTitle !== nextProps.projectTitle) {
+ this.props.onUpdateReduxProjectTitle(nextProps.projectTitle);
+ }
}
render () {
if (this.state.loadingError) {
@@ -72,10 +80,17 @@ class GUI extends React.Component {
`Failed to load project from server [id=${window.location.hash}]: ${this.state.errorMessage}`);
}
const {
+ /* eslint-disable no-unused-vars */
+ assetHost,
+ hideIntro,
+ onUpdateReduxProjectTitle,
+ projectData,
+ projectHost,
+ projectTitle,
+ /* eslint-enable no-unused-vars */
children,
fetchingProject,
loadingStateVisible,
- projectData, // eslint-disable-line no-unused-vars
vm,
...componentProps
} = this.props;
@@ -92,20 +107,25 @@ class GUI extends React.Component {
}
GUI.propTypes = {
- ...GUIComponent.propTypes,
+ assetHost: PropTypes.string,
+ children: PropTypes.node,
fetchingProject: PropTypes.bool,
+ hideIntro: PropTypes.bool,
importInfoVisible: PropTypes.bool,
loadingStateVisible: PropTypes.bool,
onSeeCommunity: PropTypes.func,
+ onUpdateProjectTitle: PropTypes.func,
+ onUpdateReduxProjectTitle: PropTypes.func,
previewInfoVisible: PropTypes.bool,
projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
+ projectHost: PropTypes.string,
+ projectTitle: PropTypes.string,
vm: PropTypes.instanceOf(VM)
};
-GUI.defaultProps = GUIComponent.defaultProps;
-
-const mapStateToProps = state => ({
+const mapStateToProps = (state, ownProps) => ({
activeTabIndex: state.scratchGui.editorTab.activeTabIndex,
+ alertsVisible: state.scratchGui.alerts.visible,
backdropLibraryVisible: state.scratchGui.modals.backdropLibrary,
blocksTabVisible: state.scratchGui.editorTab.activeTabIndex === BLOCKS_TAB_INDEX,
cardsVisible: state.scratchGui.cards.visible,
@@ -113,8 +133,9 @@ const mapStateToProps = state => ({
costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX,
importInfoVisible: state.scratchGui.modals.importInfo,
isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
+ isRtl: state.locales.isRtl,
loadingStateVisible: state.scratchGui.modals.loadingProject,
- previewInfoVisible: state.scratchGui.modals.previewInfo,
+ previewInfoVisible: state.scratchGui.modals.previewInfo && !ownProps.hideIntro,
targetIsStage: (
state.scratchGui.targets.stage &&
state.scratchGui.targets.stage.id === state.scratchGui.targets.editingTarget
@@ -129,7 +150,8 @@ const mapDispatchToProps = dispatch => ({
onActivateCostumesTab: () => dispatch(activateTab(COSTUMES_TAB_INDEX)),
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
- onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary())
+ onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
+ onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title))
});
const ConnectedGUI = connect(
diff --git a/src/containers/import-modal.jsx b/src/containers/import-modal.jsx
index 0731098f156..1c75929cf56 100644
--- a/src/containers/import-modal.jsx
+++ b/src/containers/import-modal.jsx
@@ -73,6 +73,7 @@ class ImportModal extends React.Component {
errorMessage={this.state.errorMessage}
hasValidationError={this.state.hasValidationError}
inputValue={this.state.inputValue}
+ isRtl={this.props.isRtl}
placeholder="scratch.mit.edu/projects/123456789"
onCancel={this.handleCancel}
onChange={this.handleChange}
@@ -85,12 +86,15 @@ class ImportModal extends React.Component {
}
ImportModal.propTypes = {
+ isRtl: PropTypes.bool,
onBack: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onViewProject: PropTypes.func
};
-const mapStateToProps = () => ({});
+const mapStateToProps = state => ({
+ isRtl: state.locales.isRtl
+});
const mapDispatchToProps = dispatch => ({
onBack: () => {
diff --git a/src/containers/language-selector.jsx b/src/containers/language-selector.jsx
index c3f108efbe3..8eb621bb1b7 100644
--- a/src/containers/language-selector.jsx
+++ b/src/containers/language-selector.jsx
@@ -13,11 +13,13 @@ class LanguageSelector extends React.Component {
bindAll(this, [
'handleChange'
]);
+ document.documentElement.lang = props.currentLocale;
}
handleChange (e) {
const newLocale = e.target.value;
if (this.props.supportedLocales.includes(newLocale)) {
this.props.onChangeLanguage(newLocale);
+ document.documentElement.lang = newLocale;
}
}
render () {
diff --git a/src/containers/menu-item.jsx b/src/containers/menu-item.jsx
new file mode 100644
index 00000000000..8755c19f734
--- /dev/null
+++ b/src/containers/menu-item.jsx
@@ -0,0 +1,43 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import {MenuItem as MenuItemComponent} from '../components/menu/menu.jsx';
+
+class MenuItem extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'navigateToHref'
+ ]);
+ }
+ navigateToHref () {
+ if (this.props.href) window.location.href = this.props.href;
+ }
+ render () {
+ const {
+ children,
+ className,
+ onClick
+ } = this.props;
+ const clickAction = onClick ? onClick : this.navigateToHref;
+ return (
+
+ {children}
+
+ );
+ }
+}
+
+MenuItem.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ // can take an onClick prop, or take an href and build an onClick handler
+ href: PropTypes.string,
+ onClick: PropTypes.func
+};
+
+export default MenuItem;
diff --git a/src/containers/modal.jsx b/src/containers/modal.jsx
index 67b14c9ff44..30dc4277b56 100644
--- a/src/containers/modal.jsx
+++ b/src/containers/modal.jsx
@@ -1,6 +1,7 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
+import {connect} from 'react-redux';
import ModalComponent from '../components/modal/modal.jsx';
@@ -47,8 +48,15 @@ class Modal extends React.Component {
Modal.propTypes = {
id: PropTypes.string.isRequired,
+ isRtl: PropTypes.bool,
onRequestClose: PropTypes.func,
onRequestOpen: PropTypes.func
};
-export default Modal;
+const mapStateToProps = state => ({
+ isRtl: state.locales.isRtl
+});
+
+export default connect(
+ mapStateToProps
+)(Modal);
diff --git a/src/containers/monitor.jsx b/src/containers/monitor.jsx
index b39358c08fe..2f04cb51b7f 100644
--- a/src/containers/monitor.jsx
+++ b/src/containers/monitor.jsx
@@ -7,6 +7,7 @@ import MonitorComponent, {monitorModes} from '../components/monitor/monitor.jsx'
import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout';
import {connect} from 'react-redux';
+import VM from 'scratch-vm';
const availableModes = opcode => (
monitorModes.filter(t => {
@@ -147,12 +148,14 @@ Monitor.propTypes = {
PropTypes.number
]))
]), // eslint-disable-line react/no-unused-prop-types
+ vm: PropTypes.instanceOf(VM),
width: PropTypes.number,
x: PropTypes.number,
y: PropTypes.number
};
const mapStateToProps = state => ({
- monitorLayout: state.scratchGui.monitorLayout
+ monitorLayout: state.scratchGui.monitorLayout,
+ vm: state.scratchGui.vm
});
const mapDispatchToProps = dispatch => ({
addMonitorRect: (id, rect, savePosition) =>
diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx
index 390f6ef598b..b950829028c 100644
--- a/src/containers/paint-editor-wrapper.jsx
+++ b/src/containers/paint-editor-wrapper.jsx
@@ -19,6 +19,11 @@ class PaintEditorWrapper extends React.Component {
componentDidMount () {
analytics.pageview('/editors/paint');
}
+ shouldComponentUpdate (nextProps) {
+ return this.props.imageId !== nextProps.imageId ||
+ this.props.rtl !== nextProps.rtl ||
+ this.props.name !== nextProps.name;
+ }
handleUpdateName (name) {
this.props.vm.renameCostume(this.props.selectedCostumeIndex, name);
}
@@ -40,9 +45,16 @@ class PaintEditorWrapper extends React.Component {
}
render () {
if (!this.props.imageId) return null;
+ const {
+ selectedCostumeIndex,
+ vm,
+ ...componentProps
+ } = this.props;
+
return (
@@ -56,6 +68,7 @@ PaintEditorWrapper.propTypes = {
name: PropTypes.string,
rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number,
+ rtl: PropTypes.bool,
selectedCostumeIndex: PropTypes.number.isRequired,
vm: PropTypes.instanceOf(VM)
};
@@ -73,8 +86,10 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => {
rotationCenterY: costume && costume.rotationCenterY,
imageFormat: costume && costume.dataFormat,
imageId: targetId && `${targetId}${costume.skinId}`,
- image: state.scratchGui.vm.getCostume(index),
- vm: state.scratchGui.vm
+ rtl: state.locales.isRtl,
+ selectedCostumeIndex: index,
+ vm: state.scratchGui.vm,
+ zoomLevelId: targetId
};
};
diff --git a/src/containers/preview-modal.jsx b/src/containers/preview-modal.jsx
index dd1ca670100..47b3cc3115e 100644
--- a/src/containers/preview-modal.jsx
+++ b/src/containers/preview-modal.jsx
@@ -6,8 +6,6 @@ import {connect} from 'react-redux';
import tabletFullScreen from '../lib/tablet-full-screen';
import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx';
-import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx';
-import supportedBrowser from '../lib/supported-browser';
import {
closePreviewInfo,
@@ -40,26 +38,27 @@ class PreviewModal extends React.Component {
this.props.onViewProject();
}
render () {
- return (supportedBrowser() ?
+ return (
:
-
);
}
}
PreviewModal.propTypes = {
+ isRtl: PropTypes.bool,
onTryIt: PropTypes.func,
onViewProject: PropTypes.func
};
-const mapStateToProps = () => ({});
+const mapStateToProps = state => ({
+ isRtl: state.locales.isRtl
+});
const mapDispatchToProps = dispatch => ({
onTryIt: () => {
diff --git a/src/containers/project-loader.jsx b/src/containers/project-loader.jsx
index 7f178b62324..66081b7a906 100644
--- a/src/containers/project-loader.jsx
+++ b/src/containers/project-loader.jsx
@@ -2,8 +2,11 @@ import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
+import {defineMessages, injectIntl, intlShape} from 'react-intl';
import analytics from '../lib/analytics';
+import log from '../lib/log';
+import {setProjectTitle} from '../reducers/project-title';
import {
openLoadingProject,
@@ -26,6 +29,15 @@ import {
*
* )}
*/
+
+const messages = defineMessages({
+ loadError: {
+ id: 'gui.projectLoader.loadError',
+ defaultMessage: 'The project file that was selected failed to load.',
+ description: 'An error that displays when a local project file fails to load.'
+ }
+});
+
class ProjectLoader extends React.Component {
constructor (props) {
super(props);
@@ -35,10 +47,6 @@ class ProjectLoader extends React.Component {
'handleChange',
'handleClick'
]);
- this.state = {
- loadingError: false,
- errorMessage: ''
- };
}
handleChange (e) {
// Remove the hash if any (without triggering a hash change event or a reload)
@@ -58,11 +66,22 @@ class ProjectLoader extends React.Component {
thisFileInput.value = null;
})
.catch(error => {
- this.setState({loadingError: true, errorMessage: error});
+ log.warn(error);
+ alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
+ this.props.closeLoadingState();
+ // Reset the file input after project is loaded
+ // This is necessary in case the user wants to reload a project
+ thisFileInput.value = null;
});
if (thisFileInput.files) { // Don't attempt to load if no file was selected
this.props.openLoadingState();
reader.readAsArrayBuffer(thisFileInput.files[0]);
+ if (thisFileInput.files[0].name) {
+ const matches = thisFileInput.files[0].name.match(/^(.*)\.sb3$/);
+ if (matches) {
+ this.props.onSetProjectTitle(matches[1].substring(0, 100));
+ }
+ }
}
}
handleClick () {
@@ -83,10 +102,6 @@ class ProjectLoader extends React.Component {
);
}
render () {
- if (this.state.loadingError) {
- throw new Error(
- `Failed to load project from file: ${this.state.errorMessage}`);
- }
const {
/* eslint-disable no-unused-vars */
children,
@@ -103,6 +118,8 @@ class ProjectLoader extends React.Component {
ProjectLoader.propTypes = {
children: PropTypes.func,
closeLoadingState: PropTypes.func,
+ intl: intlShape.isRequired,
+ onSetProjectTitle: PropTypes.func,
openLoadingState: PropTypes.func,
vm: PropTypes.shape({
loadProject: PropTypes.func
@@ -115,10 +132,11 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
closeLoadingState: () => dispatch(closeLoadingProject()),
+ onSetProjectTitle: title => dispatch(setProjectTitle(title)),
openLoadingState: () => dispatch(openLoadingProject())
});
export default connect(
mapStateToProps,
mapDispatchToProps
-)(ProjectLoader);
+)(injectIntl(ProjectLoader));
diff --git a/src/containers/project-saver.jsx b/src/containers/project-saver.jsx
index c594421eca3..c32d4b1bf0a 100644
--- a/src/containers/project-saver.jsx
+++ b/src/containers/project-saver.jsx
@@ -2,6 +2,9 @@ import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
+import storage from '../lib/storage';
+import {projectTitleInitialState} from '../reducers/project-title';
+
/**
* Project saver component passes a saveProject function to its child.
@@ -21,55 +24,83 @@ class ProjectSaver extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
- 'saveProject'
+ 'createProject',
+ 'updateProject',
+ 'saveProject',
+ 'doStoreProject'
]);
}
saveProject () {
const saveLink = document.createElement('a');
document.body.appendChild(saveLink);
- this.props.vm.saveProjectSb3().then(content => {
- // TODO user-friendly project name
- // File name: project-DATE-TIME
- const date = new Date();
- const timestamp = `${date.toLocaleDateString()}-${date.toLocaleTimeString()}`;
- const filename = `untitled-project-${timestamp}.sb3`;
-
+ this.props.saveProjectSb3().then(content => {
// Use special ms version if available to get it working on Edge.
if (navigator.msSaveOrOpenBlob) {
- navigator.msSaveOrOpenBlob(content, filename);
+ navigator.msSaveOrOpenBlob(content, this.props.projectFilename);
return;
}
const url = window.URL.createObjectURL(content);
saveLink.href = url;
- saveLink.download = filename;
+ saveLink.download = this.props.projectFilename;
saveLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(saveLink);
});
}
+ doStoreProject (id) {
+ return this.props.saveProjectSb3()
+ .then(content => {
+ const assetType = storage.AssetType.Project;
+ const dataFormat = storage.DataFormat.SB3;
+ const body = new FormData();
+ body.append('sb3_file', content, 'sb3_file');
+ return storage.store(
+ assetType,
+ dataFormat,
+ body,
+ id
+ );
+ });
+ }
+ createProject () {
+ return this.doStoreProject();
+ }
+ updateProject () {
+ return this.doStoreProject(this.props.projectId);
+ }
render () {
const {
- /* eslint-disable no-unused-vars */
- children,
- vm,
- /* eslint-enable no-unused-vars */
- ...props
+ children
} = this.props;
- return this.props.children(this.saveProject, props);
+ return children(
+ this.saveProject,
+ this.updateProject,
+ this.createProject
+ );
}
}
+const getProjectFilename = (curTitle, defaultTitle) => {
+ let filenameTitle = curTitle;
+ if (!filenameTitle || filenameTitle.length === 0) {
+ filenameTitle = defaultTitle;
+ }
+ return `${filenameTitle.substring(0, 100)}.sb3`;
+};
+
ProjectSaver.propTypes = {
children: PropTypes.func,
- vm: PropTypes.shape({
- saveProjectSb3: PropTypes.func
- })
+ projectFilename: PropTypes.string,
+ projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ saveProjectSb3: PropTypes.func
};
const mapStateToProps = state => ({
- vm: state.scratchGui.vm
+ saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm),
+ projectFilename: getProjectFilename(state.scratchGui.projectTitle, projectTitleInitialState),
+ projectId: state.scratchGui.projectId
});
export default connect(
diff --git a/src/containers/scanning-step.jsx b/src/containers/scanning-step.jsx
index ecc19318913..6e1daa8e6eb 100644
--- a/src/containers/scanning-step.jsx
+++ b/src/containers/scanning-step.jsx
@@ -14,46 +14,50 @@ class ScanningStep extends React.Component {
]);
this.state = {
scanning: true,
- deviceList: []
+ peripheralList: []
};
}
componentDidMount () {
- this.props.vm.startDeviceScan(this.props.extensionId);
+ this.props.vm.scanForPeripheral(this.props.extensionId);
this.props.vm.on(
'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate);
this.props.vm.on(
'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout);
}
componentWillUnmount () {
- // @todo: stop the device scan here
+ // @todo: stop the peripheral scan here
this.props.vm.removeListener(
'PERIPHERAL_LIST_UPDATE', this.handlePeripheralListUpdate);
this.props.vm.removeListener(
'PERIPHERAL_SCAN_TIMEOUT', this.handlePeripheralScanTimeout);
}
handlePeripheralScanTimeout () {
- this.setState({scanning: false});
+ this.setState({
+ scanning: false,
+ peripheralList: []
+ });
}
handlePeripheralListUpdate (newList) {
// TODO: sort peripherals by signal strength? so they don't jump around
const peripheralArray = Object.keys(newList).map(id =>
newList[id]
);
- this.setState({deviceList: peripheralArray});
+ this.setState({peripheralList: peripheralArray});
}
handleRefresh () {
- this.props.vm.startDeviceScan(this.props.extensionId);
+ this.props.vm.scanForPeripheral(this.props.extensionId);
this.setState({
scanning: true,
- deviceList: []
+ peripheralList: []
});
}
render () {
return (
({
+ isRtl: state.locales.isRtl
+});
+
+const mapDispatchToProps = () => ({});
+
+export default injectIntl(connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(SoundLibrary));
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 2245f3d2f6a..9631b30d358 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -6,6 +6,7 @@ import VM from 'scratch-vm';
import AssetPanel from '../components/asset-panel/asset-panel.jsx';
import soundIcon from '../components/asset-panel/icon--sound.svg';
+import soundIconRtl from '../components/asset-panel/icon--sound-rtl.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';
@@ -34,6 +35,8 @@ import {
COSTUMES_TAB_INDEX
} from '../reducers/editor-tab';
+import {setRestore} from '../reducers/restore-deletion';
+
class SoundTab extends React.Component {
constructor (props) {
super(props);
@@ -76,10 +79,11 @@ class SoundTab extends React.Component {
}
handleDeleteSound (soundIndex) {
- this.props.vm.deleteSound(soundIndex);
+ const restoreFun = this.props.vm.deleteSound(soundIndex);
if (soundIndex >= this.state.selectedSoundIndex) {
this.setState({selectedSoundIndex: Math.max(0, soundIndex - 1)});
}
+ this.props.dispatchUpdateRestore({restoreFun, deletedItem: 'Sound'});
}
handleDuplicateSound (soundIndex) {
@@ -153,7 +157,9 @@ class SoundTab extends React.Component {
render () {
const {
+ dispatchUpdateRestore, // eslint-disable-line no-unused-vars
intl,
+ isRtl,
vm,
onNewSoundFromLibraryClick,
onNewSoundFromRecordingClick
@@ -167,7 +173,7 @@ class SoundTab extends React.Component {
const sounds = sprite.sounds ? sprite.sounds.map(sound => (
{
- url: soundIcon,
+ url: isRtl ? soundIconRtl : soundIcon,
name: sound.name,
details: (sound.sampleCount / sound.rate).toFixed(2),
dragPayload: sound
@@ -252,8 +258,10 @@ class SoundTab extends React.Component {
}
SoundTab.propTypes = {
+ dispatchUpdateRestore: PropTypes.func,
editingTarget: PropTypes.string,
intl: intlShape,
+ isRtl: PropTypes.bool,
onActivateCostumesTab: PropTypes.func.isRequired,
onNewSoundFromLibraryClick: PropTypes.func.isRequired,
onNewSoundFromRecordingClick: PropTypes.func.isRequired,
@@ -277,6 +285,7 @@ SoundTab.propTypes = {
const mapStateToProps = state => ({
editingTarget: state.scratchGui.targets.editingTarget,
+ isRtl: state.locales.isRtl,
sprites: state.scratchGui.targets.sprites,
stage: state.scratchGui.targets.stage,
soundLibraryVisible: state.scratchGui.modals.soundLibrary,
@@ -294,6 +303,9 @@ const mapDispatchToProps = dispatch => ({
},
onRequestCloseSoundLibrary: () => {
dispatch(closeSoundLibrary());
+ },
+ dispatchUpdateRestore: restoreState => {
+ dispatch(setRestore(restoreState));
}
});
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index 785f9d08f49..9b154c59b0a 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -6,15 +6,20 @@ import {connect} from 'react-redux';
import {setHoveredSprite} from '../reducers/hovered-target';
import {updateAssetDrag} from '../reducers/asset-drag';
import {getEventXY} from '../lib/touch-utils';
+import VM from 'scratch-vm';
+import {SVGRenderer} from 'scratch-svg-renderer';
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
const dragThreshold = 3; // Same as the block drag threshold
+// Contains 'font-family', but doesn't only contain 'font-family="none"'
+const HAS_FONT_REGEXP = 'font-family(?!="none")';
class SpriteSelectorItem extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
+ 'getCostumeUrl',
'handleClick',
'handleDelete',
'handleDuplicate',
@@ -25,6 +30,46 @@ class SpriteSelectorItem extends React.Component {
'handleMouseMove',
'handleMouseUp'
]);
+ this.svgRenderer = new SVGRenderer();
+ // Asset ID of the SVG currently in SVGRenderer
+ this.decodedAssetId = null;
+ }
+ shouldComponentUpdate (nextProps) {
+ // Ignore dragPayload due to https://github.com/LLK/scratch-gui/issues/3172.
+ // This function should be removed once the issue is fixed.
+ for (const property in nextProps) {
+ if (property !== 'dragPayload' && this.props[property] !== nextProps[property]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ getCostumeUrl () {
+ if (this.props.costumeURL) return this.props.costumeURL;
+ if (!this.props.assetId) return null;
+
+ const storage = this.props.vm.runtime.storage;
+ const asset = storage.get(this.props.assetId);
+ // If the SVG refers to fonts, they must be inlined in order to display correctly in the img tag.
+ // Avoid parsing the SVG when possible, since it's expensive.
+ if (asset.assetType === storage.AssetType.ImageVector) {
+ // If the asset ID has not changed, no need to re-parse
+ if (this.decodedAssetId === this.props.assetId) {
+ // @todo consider caching more than one URL.
+ return this.cachedUrl;
+ }
+ this.decodedAssetId = this.props.assetId;
+ const svgString = this.props.vm.runtime.storage.get(this.props.assetId).decodeText();
+ if (svgString.match(HAS_FONT_REGEXP)) {
+ this.svgRenderer.loadString(svgString);
+ const svgText = this.svgRenderer.toString(true /* shouldInjectFonts */);
+ this.cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
+ } else {
+ this.cachedUrl = this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI();
+ }
+ return this.cachedUrl;
+ }
+ return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI();
}
handleMouseUp () {
this.initialOffset = null;
@@ -49,7 +94,7 @@ class SpriteSelectorItem extends React.Component {
const dy = currentOffset.y - this.initialOffset.y;
if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) {
this.props.onDrag({
- img: this.props.costumeURL,
+ img: this.getCostumeUrl(),
currentOffset: currentOffset,
dragging: true,
dragType: this.props.dragType,
@@ -75,11 +120,7 @@ class SpriteSelectorItem extends React.Component {
}
handleDelete (e) {
e.stopPropagation(); // To prevent from bubbling back to handleClick
- // @todo add i18n here
- // eslint-disable-next-line no-alert
- if (window.confirm('Are you sure you want to delete this?')) {
- this.props.onDeleteButtonClick(this.props.id);
- }
+ this.props.onDeleteButtonClick(this.props.id);
}
handleDuplicate (e) {
e.stopPropagation(); // To prevent from bubbling back to handleClick
@@ -107,11 +148,14 @@ class SpriteSelectorItem extends React.Component {
onExportButtonClick,
dragPayload,
receivedBlocks,
+ costumeURL,
+ vm,
/* eslint-enable no-unused-vars */
...props
} = this.props;
return (
({
- costumeURL: costumeURL || (assetId && state.scratchGui.vm.runtime.storage.get(assetId).encodeDataURI()),
+const mapStateToProps = (state, {id}) => ({
dragging: state.scratchGui.assetDrag.dragging,
receivedBlocks: state.scratchGui.hoveredTarget.receivedBlocks &&
- state.scratchGui.hoveredTarget.sprite === id
+ state.scratchGui.hoveredTarget.sprite === id,
+ vm: state.scratchGui.vm
});
const mapDispatchToProps = dispatch => ({
dispatchSetHoveredSprite: spriteId => {
@@ -159,7 +204,12 @@ const mapDispatchToProps = dispatch => ({
onDrag: data => dispatch(updateAssetDrag(data))
});
-export default connect(
+const ConnectedComponent = connect(
mapStateToProps,
mapDispatchToProps
)(SpriteSelectorItem);
+
+export {
+ ConnectedComponent as default,
+ HAS_FONT_REGEXP // Exposed for testing
+};
diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx
index 99c233c7d8e..ad302d51d3b 100644
--- a/src/containers/stage-selector.jsx
+++ b/src/containers/stage-selector.jsx
@@ -2,18 +2,31 @@ import bindAll from 'lodash.bindall';
import omit from 'lodash.omit';
import PropTypes from 'prop-types';
import React from 'react';
+import {intlShape, injectIntl} from 'react-intl';
import {connect} from 'react-redux';
import {openBackdropLibrary} from '../reducers/modals';
import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab';
import {setHoveredSprite} from '../reducers/hovered-target';
+import DragConstants from '../lib/drag-constants';
+import DropAreaHOC from '../lib/drop-area-hoc.jsx';
+import {emptyCostume} from '../lib/empty-assets';
+import sharedMessages from '../lib/shared-messages';
import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx';
import backdropLibraryContent from '../lib/libraries/backdrops.json';
-import costumeLibraryContent from '../lib/libraries/costumes.json';
import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js';
+const dragTypes = [
+ DragConstants.COSTUME,
+ DragConstants.SOUND,
+ DragConstants.BACKPACK_COSTUME,
+ DragConstants.BACKPACK_SOUND
+];
+
+const DroppableStage = DropAreaHOC(dragTypes)(StageSelectorComponent);
+
class StageSelector extends React.Component {
constructor (props) {
super(props);
@@ -27,6 +40,7 @@ class StageSelector extends React.Component {
'handleBackdropUpload',
'handleMouseEnter',
'handleMouseLeave',
+ 'handleDrop',
'setFileInput'
]);
}
@@ -54,11 +68,7 @@ class StageSelector extends React.Component {
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);
- }
+ this.handleNewBackdrop(emptyCostume(this.props.intl.formatMessage(sharedMessages.backdrop, {index: 1})));
}
handleBackdropUpload (e) {
const storage = this.props.vm.runtime.storage;
@@ -75,23 +85,39 @@ class StageSelector extends React.Component {
handleMouseLeave () {
this.props.dispatchSetHoveredSprite(null);
}
+ handleDrop (dragInfo) {
+ if (dragInfo.dragType === DragConstants.COSTUME) {
+ this.props.vm.shareCostumeToTarget(dragInfo.index, this.props.id);
+ } else if (dragInfo.dragType === DragConstants.SOUND) {
+ this.props.vm.shareSoundToTarget(dragInfo.index, this.props.id);
+ } else if (dragInfo.dragType === DragConstants.BACKPACK_COSTUME) {
+ this.props.vm.addCostume(dragInfo.payload.body, {
+ name: dragInfo.payload.name
+ }, this.props.id);
+ } else if (dragInfo.dragType === DragConstants.BACKPACK_SOUND) {
+ this.props.vm.addSound({
+ md5: dragInfo.payload.body,
+ name: dragInfo.payload.name
+ }, this.props.id);
+ }
+ }
setFileInput (input) {
this.fileInput = input;
}
render () {
const componentProps = omit(this.props, [
- 'assetId', 'dispatchSetHoveredSprite', 'id', 'onActivateTab', 'onSelect']);
+ 'assetId', 'dispatchSetHoveredSprite', 'id', 'intl', 'onActivateTab', 'onSelect']);
return (
-
);
@@ -100,6 +126,7 @@ class StageSelector extends React.Component {
StageSelector.propTypes = {
...StageSelectorComponent.propTypes,
id: PropTypes.string,
+ intl: intlShape.isRequired,
onSelect: PropTypes.func
};
@@ -124,7 +151,7 @@ const mapDispatchToProps = dispatch => ({
}
});
-export default connect(
+export default injectIntl(connect(
mapStateToProps,
mapDispatchToProps
-)(StageSelector);
+)(StageSelector));
diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx
index 0ff48ef0216..dc2b8704395 100644
--- a/src/containers/stage.jsx
+++ b/src/containers/stage.jsx
@@ -52,9 +52,9 @@ class Stage extends React.Component {
colorInfo: null,
question: null
};
- if (this.props.vm.runtime.renderer) {
- this.renderer = this.props.vm.runtime.renderer;
- this.canvas = this.props.vm.runtime.renderer._gl.canvas;
+ if (this.props.vm.renderer) {
+ this.renderer = this.props.vm.renderer;
+ this.canvas = this.renderer.canvas;
} else {
this.canvas = document.createElement('canvas');
this.renderer = new Renderer(this.canvas);
@@ -75,7 +75,8 @@ class Stage extends React.Component {
this.props.isColorPicking !== nextProps.isColorPicking ||
this.state.colorInfo !== nextState.colorInfo ||
this.props.isFullScreen !== nextProps.isFullScreen ||
- this.state.question !== nextState.question;
+ this.state.question !== nextState.question ||
+ this.props.micIndicator !== nextProps.micIndicator;
}
componentDidUpdate (prevProps) {
if (this.props.isColorPicking && !prevProps.isColorPicking) {
@@ -309,11 +310,13 @@ class Stage extends React.Component {
const targetId = this.props.vm.getTargetIdForDrawableId(drawableId);
if (targetId === null) return;
- // Only start drags on non-draggable targets in editor drag mode
- if (!this.props.useEditorDragStyle) {
- const target = this.props.vm.runtime.getTargetById(targetId);
- if (!target.draggable) return;
- }
+ const target = this.props.vm.runtime.getTargetById(targetId);
+
+ // Do not start drag unless in editor drag mode or target is draggable
+ if (!(this.props.useEditorDragStyle || target.draggable)) return;
+
+ // Dragging always brings the target to the front
+ target.goToFront();
this.props.vm.startDrag(targetId);
this.setState({
@@ -400,6 +403,7 @@ Stage.defaultProps = {
const mapStateToProps = state => ({
isColorPicking: state.scratchGui.colorPicker.active,
isFullScreen: state.scratchGui.mode.isFullScreen,
+ micIndicator: state.scratchGui.micIndicator,
// Do not use editor drag style in fullscreen or player mode.
useEditorDragStyle: !(state.scratchGui.mode.isFullScreen || state.scratchGui.mode.isPlayerOnly)
});
diff --git a/src/containers/tag-button.jsx b/src/containers/tag-button.jsx
index e5ad61f4080..3e7dd84b1bd 100644
--- a/src/containers/tag-button.jsx
+++ b/src/containers/tag-button.jsx
@@ -12,7 +12,7 @@ class TagButton extends React.Component {
]);
}
handleClick () {
- this.props.onClick(this.props.title);
+ this.props.onClick(this.props.tag);
}
render () {
return (
@@ -26,11 +26,7 @@ class TagButton extends React.Component {
TagButton.propTypes = {
...TagButtonComponent.propTypes,
- onClick: PropTypes.func,
- title: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.object
- ]).isRequired
+ onClick: PropTypes.func
};
export default TagButton;
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index 5f9fef8db72..a2e5fdcc5f1 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -2,6 +2,7 @@ import bindAll from 'lodash.bindall';
import React from 'react';
import {connect} from 'react-redux';
+import {intlShape, injectIntl} from 'react-intl';
import {
openSpriteLibrary,
@@ -10,16 +11,20 @@ import {
import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab';
import {setReceivedBlocks} from '../reducers/hovered-target';
+import {setRestore} from '../reducers/restore-deletion';
import DragConstants from '../lib/drag-constants';
import TargetPaneComponent from '../components/target-pane/target-pane.jsx';
import spriteLibraryContent from '../lib/libraries/sprites.json';
import {handleFileUpload, spriteUpload} from '../lib/file-uploader.js';
+import sharedMessages from '../lib/shared-messages';
+import {emptySprite} from '../lib/empty-assets';
class TargetPane extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleBlockDragEnd',
+ 'handleChangeSpriteRotationStyle',
'handleChangeSpriteDirection',
'handleChangeSpriteName',
'handleChangeSpriteSize',
@@ -48,6 +53,9 @@ class TargetPane extends React.Component {
handleChangeSpriteDirection (direction) {
this.props.vm.postSpriteInfo({direction});
}
+ handleChangeSpriteRotationStyle (rotationStyle) {
+ this.props.vm.postSpriteInfo({rotationStyle});
+ }
handleChangeSpriteName (name) {
this.props.vm.renameSprite(this.props.editingTarget, name);
}
@@ -64,7 +72,12 @@ class TargetPane extends React.Component {
this.props.vm.postSpriteInfo({y});
}
handleDeleteSprite (id) {
- this.props.vm.deleteSprite(id);
+ const restoreFun = this.props.vm.deleteSprite(id);
+ this.props.dispatchUpdateRestore({
+ restoreFun: restoreFun,
+ deletedItem: 'Sprite'
+ });
+
}
handleDuplicateSprite (id) {
this.props.vm.duplicateSprite(id);
@@ -99,15 +112,17 @@ class TargetPane extends React.Component {
this.props.vm.addSprite(JSON.stringify(item.json));
}
handlePaintSpriteClick () {
- // @todo this is brittle, will need to be refactored for localized libraries
- const emptyItem = spriteLibraryContent.find(item => item.name === 'Empty');
- if (emptyItem) {
- this.props.vm.addSprite(JSON.stringify(emptyItem.json)).then(() => {
- setTimeout(() => { // Wait for targets update to propagate before tab switching
- this.props.onActivateTab(COSTUMES_TAB_INDEX);
- });
+ const formatMessage = this.props.intl.formatMessage;
+ const emptyItem = emptySprite(
+ formatMessage(sharedMessages.sprite, {index: 1}),
+ formatMessage(sharedMessages.pop),
+ formatMessage(sharedMessages.costume, {index: 1})
+ );
+ this.props.vm.addSprite(JSON.stringify(emptyItem)).then(() => {
+ setTimeout(() => { // Wait for targets update to propagate before tab switching
+ this.props.onActivateTab(COSTUMES_TAB_INDEX);
});
- }
+ });
}
handleNewSprite (spriteJSONString) {
this.props.vm.addSprite(spriteJSONString);
@@ -117,8 +132,9 @@ class TargetPane extends React.Component {
}
handleSpriteUpload (e) {
const storage = this.props.vm.runtime.storage;
+ const costumeSuffix = this.props.intl.formatMessage(sharedMessages.costume, {index: 1});
handleFileUpload(e.target, (buffer, fileType, fileName) => {
- spriteUpload(buffer, fileType, fileName, storage, this.handleNewSprite);
+ spriteUpload(buffer, fileType, fileName, storage, this.handleNewSprite, costumeSuffix);
});
}
setFileInput (input) {
@@ -126,7 +142,7 @@ class TargetPane extends React.Component {
}
handleBlockDragEnd (blocks) {
if (this.props.hoveredTarget.sprite && this.props.hoveredTarget.sprite !== this.props.editingTarget) {
- this.props.vm.shareBlocksToTarget(blocks, this.props.hoveredTarget.sprite);
+ this.props.vm.shareBlocksToTarget(blocks, this.props.hoveredTarget.sprite, this.props.editingTarget);
this.props.onReceivedBlocks(true);
}
}
@@ -152,6 +168,18 @@ class TargetPane extends React.Component {
this.props.vm.shareCostumeToTarget(dragInfo.index, targetId);
} else if (targetId && dragInfo.dragType === DragConstants.SOUND) {
this.props.vm.shareSoundToTarget(dragInfo.index, targetId);
+ } else if (dragInfo.dragType === DragConstants.BACKPACK_COSTUME) {
+ // In scratch 2, this only creates a new sprite from the costume.
+ // We may be able to handle both kinds of drops, depending on where
+ // the drop happens. For now, just add the costume.
+ this.props.vm.addCostume(dragInfo.payload.body, {
+ name: dragInfo.payload.name
+ }, targetId);
+ } else if (dragInfo.dragType === DragConstants.BACKPACK_SOUND) {
+ this.props.vm.addSound({
+ md5: dragInfo.payload.body,
+ name: dragInfo.payload.name
+ }, targetId);
}
}
}
@@ -159,6 +187,7 @@ class TargetPane extends React.Component {
const {
onActivateTab, // eslint-disable-line no-unused-vars
onReceivedBlocks, // eslint-disable-line no-unused-vars
+ dispatchUpdateRestore, // eslint-disable-line no-unused-vars
...componentProps
} = this.props;
return (
@@ -167,6 +196,7 @@ class TargetPane extends React.Component {
fileInputRef={this.setFileInput}
onChangeSpriteDirection={this.handleChangeSpriteDirection}
onChangeSpriteName={this.handleChangeSpriteName}
+ onChangeSpriteRotationStyle={this.handleChangeSpriteRotationStyle}
onChangeSpriteSize={this.handleChangeSpriteSize}
onChangeSpriteVisibility={this.handleChangeSpriteVisibility}
onChangeSpriteX={this.handleChangeSpriteX}
@@ -191,6 +221,7 @@ const {
} = TargetPaneComponent.propTypes;
TargetPane.propTypes = {
+ intl: intlShape.isRequired,
...targetPaneProps
};
@@ -223,10 +254,13 @@ const mapDispatchToProps = dispatch => ({
},
onReceivedBlocks: receivedBlocks => {
dispatch(setReceivedBlocks(receivedBlocks));
+ },
+ dispatchUpdateRestore: restoreState => {
+ dispatch(setRestore(restoreState));
}
});
-export default connect(
+export default injectIntl(connect(
mapStateToProps,
mapDispatchToProps
-)(TargetPane);
+)(TargetPane));
diff --git a/src/containers/tips-library.jsx b/src/containers/tips-library.jsx
index ebbee3f3022..009c8c2fae9 100644
--- a/src/containers/tips-library.jsx
+++ b/src/containers/tips-library.jsx
@@ -54,6 +54,7 @@ class TipsLibrary extends React.PureComponent {
{(toggleTurboMode, props) => (
+ *
+ * )}
+ */
+class TurboMode extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'toggleTurboMode'
+ ]);
+ }
+ toggleTurboMode () {
+ this.props.vm.setTurboMode(!this.props.turboMode);
+ }
+ render () {
+ const {
+ /* eslint-disable no-unused-vars */
+ children,
+ vm,
+ /* eslint-enable no-unused-vars */
+ ...props
+ } = this.props;
+ return this.props.children(this.toggleTurboMode, props);
+ }
+}
+
+TurboMode.propTypes = {
+ children: PropTypes.func,
+ turboMode: PropTypes.bool,
+ vm: PropTypes.shape({
+ setTurboMode: PropTypes.func
+ })
+};
+
+const mapStateToProps = state => ({
+ vm: state.scratchGui.vm,
+ turboMode: state.scratchGui.vmStatus.turbo
+});
+
+export default connect(
+ mapStateToProps,
+ () => ({}) // omit dispatch prop
+)(TurboMode);
diff --git a/src/containers/webgl-modal.jsx b/src/containers/webgl-modal.jsx
index 02e43bf01db..d6633f30394 100644
--- a/src/containers/webgl-modal.jsx
+++ b/src/containers/webgl-modal.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import PropTypes from 'prop-types';
import WebGlModalComponent from '../components/webgl-modal/webgl-modal.jsx';
@@ -9,10 +10,15 @@ class WebGlModal extends React.Component {
render () {
return (
);
}
}
+WebGlModal.propTypes = {
+ isRtl: PropTypes.bool
+};
+
export default WebGlModal;
diff --git a/src/css/colors.css b/src/css/colors.css
index 37ffa62227a..2222608922a 100644
--- a/src/css/colors.css
+++ b/src/css/colors.css
@@ -6,6 +6,7 @@ $ui-modal-overlay: hsla(215, 100%, 65%, 0.9); /* 90% transparent version of moti
$ui-white: hsla(0, 100%, 100%, 1); /* #FFFFFF */
$ui-white-transparent: hsla(0, 100%, 100%, 0.25); /* 25% transparent version of ui-white */
+$ui-transparent: hsla(0, 100%, 100%, 0); /* 25% transparent version of ui-white */
$ui-black-transparent: hsla(0, 0%, 0%, 0.15); /* 15% transparent version of black */
diff --git a/src/css/units.css b/src/css/units.css
index ef34280983f..db51ed00934 100644
--- a/src/css/units.css
+++ b/src/css/units.css
@@ -1,14 +1,22 @@
+/* make sure to keep these in sync with other constants,
+e.g. STAGE_DIMENSION_DEFAULTS in lib/screen-utils.js */
+
$space: 0.5rem;
$sprites-per-row: 5;
$menu-bar-height: 3rem;
+$language-selector-width: 3rem;
$sprite-info-height: 6rem;
$stage-menu-height: 2.75rem;
$library-header-height: 3.125rem;
$library-filter-bar-height: 2.5rem;
+$stage-standard-border-width: 0.0625rem;
+$stage-full-screen-border-width: 0.1875rem;
+$stage-full-screen-stage-padding: 0.1875rem;
+
$form-radius: calc($space / 2);
/* layout contants from `layout-constants.js` */
diff --git a/src/css/z-index.css b/src/css/z-index.css
index b3368fc2bd6..06e2015658b 100644
--- a/src/css/z-index.css
+++ b/src/css/z-index.css
@@ -8,8 +8,9 @@ $z-index-extension-button: 50; /* Force extension button above the ScratchBlocks
$z-index-menu-bar: 50; /* blocklyToolboxDiv is 40 */
$z-index-monitor: 100;
-$z-index-coming-soon: 110;
+$z-index-stage-indicator: 110;
$z-index-add-button: 120;
+$z-index-tooltip: 130; /* tooltips should go over add buttons if they overlap */
$z-index-card: 490;
$z-index-loader: 500;
@@ -23,6 +24,7 @@ $z-index-stage-color-picker-background: 2000;
$z-index-stage-with-color-picker: 2010;
$z-index-stage-header: 5000;
$z-index-stage-wrapper-overlay: 5000;
+$z-index-alerts: 5010;
/* in most interfaces, the context menu is always on top */
$z-index-context-menu: 10000;
diff --git a/src/index.js b/src/index.js
index dfd32a399de..237c1093d65 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,5 @@
import GUI from './containers/gui.jsx';
+import AppStateHOC from './lib/app-state-hoc.jsx';
import GuiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from './reducers/gui';
import LocalesReducer, {localesInitialState, initLocale} from './reducers/locales';
import {ScratchPaintReducer} from 'scratch-paint';
@@ -13,6 +14,7 @@ const guiReducers = {
export {
GUI as default,
+ AppStateHOC,
setAppElement,
guiReducers,
guiInitialState,
diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx
index 2f70b44d251..93d97180ef3 100644
--- a/src/lib/app-state-hoc.jsx
+++ b/src/lib/app-state-hoc.jsx
@@ -4,57 +4,95 @@ import {Provider} from 'react-redux';
import {createStore, combineReducers, compose} from 'redux';
import ConnectedIntlProvider from './connected-intl-provider.jsx';
-import guiReducer, {guiInitialState, guiMiddleware, initFullScreen, initPlayer} from '../reducers/gui';
import localesReducer, {initLocale, localesInitialState} from '../reducers/locales';
import {setPlayer, setFullScreen} from '../reducers/mode.js';
import locales from 'scratch-l10n';
import {detectLocale} from './detect-locale';
-
-import {ScratchPaintReducer} from 'scratch-paint';
+import {detectTutorialId} from './tutorial-from-url';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
-const enhancer = composeEnhancers(guiMiddleware);
/*
* Higher Order Component to provide redux state. If an `intl` prop is provided
* it will override the internal `intl` redux state
* @param {React.Component} WrappedComponent - component to provide state for
+ * @param {boolean} localesOnly - only provide the locale state, not everything
+ * required by the GUI. Used to exclude excess state when
+ only rendering modals, not the GUI.
* @returns {React.Component} component with redux and intl state provided
*/
-const AppStateHOC = function (WrappedComponent) {
+const AppStateHOC = function (WrappedComponent, localesOnly) {
class AppStateWrapper extends React.Component {
constructor (props) {
super(props);
- let initializedGui = guiInitialState;
- if (props.isFullScreen) {
- initializedGui = initFullScreen(initializedGui);
- }
- if (props.isPlayerOnly) {
- initializedGui = initPlayer(initializedGui);
- }
+ let initialState = {};
+ let reducers = {};
+ let enhancer;
let initializedLocales = localesInitialState;
const locale = detectLocale(Object.keys(locales));
if (locale !== 'en') {
initializedLocales = initLocale(initializedLocales, locale);
}
+ if (localesOnly) {
+ // Used for instantiating minimal state for the unsupported
+ // browser modal
+ reducers = {locales: localesReducer};
+ initialState = {locales: initializedLocales};
+ enhancer = composeEnhancers();
+ } else {
+ // You are right, this is gross. But it's necessary to avoid
+ // importing unneeded code that will crash unsupported browsers.
+ const guiRedux = require('../reducers/gui');
+ const guiReducer = guiRedux.default;
+ const {
+ guiInitialState,
+ guiMiddleware,
+ initFullScreen,
+ initPlayer,
+ initTutorialCard
+ } = guiRedux;
+ const {ScratchPaintReducer} = require('scratch-paint');
- const reducer = combineReducers({
- locales: localesReducer,
- scratchGui: guiReducer,
- scratchPaint: ScratchPaintReducer
- });
- this.store = createStore(
- reducer,
- {
+ let initializedGui = guiInitialState;
+ if (props.isFullScreen || props.isPlayerOnly) {
+ if (props.isFullScreen) {
+ initializedGui = initFullScreen(initializedGui);
+ }
+ if (props.isPlayerOnly) {
+ initializedGui = initPlayer(initializedGui);
+ }
+ } else {
+ const tutorialId = detectTutorialId();
+ if (tutorialId !== null) {
+ // When loading a tutorial from the URL,
+ // load w/o preview modal
+ // open requested tutorial card
+ initializedGui = initTutorialCard(initializedGui, tutorialId);
+ }
+ }
+ reducers = {
+ locales: localesReducer,
+ scratchGui: guiReducer,
+ scratchPaint: ScratchPaintReducer
+ };
+ initialState = {
locales: initializedLocales,
scratchGui: initializedGui
- },
- enhancer);
+ };
+ enhancer = composeEnhancers(guiMiddleware);
+ }
+ const reducer = combineReducers(reducers);
+ this.store = createStore(
+ reducer,
+ initialState,
+ enhancer
+ );
}
componentDidUpdate (prevProps) {
+ if (localesOnly) return;
if (prevProps.isPlayerOnly !== this.props.isPlayerOnly) {
this.store.dispatch(setPlayer(this.props.isPlayerOnly));
}
diff --git a/src/lib/assets/icon--help.svg b/src/lib/assets/icon--help.svg
new file mode 100644
index 00000000000..2938e8340eb
--- /dev/null
+++ b/src/lib/assets/icon--help.svg
@@ -0,0 +1,10 @@
+
+
+
+ help
+ Created with Sketch.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js
index f0c51f14040..9453f197709 100644
--- a/src/lib/backpack-api.js
+++ b/src/lib/backpack-api.js
@@ -12,7 +12,7 @@ const getBackpackContents = ({
}) => new Promise((resolve, reject) => {
xhr({
method: 'GET',
- uri: `${host}${username}?limit=${limit}&offset=${offset}`,
+ uri: `${host}/${username}?limit=${limit}&offset=${offset}`,
headers: {'x-token': token},
json: true
}, (error, response) => {
@@ -43,7 +43,7 @@ const saveBackpackObject = ({
}) => new Promise((resolve, reject) => {
xhr({
method: 'POST',
- uri: `${host}${username}`,
+ uri: `${host}/${username}`,
headers: {'x-token': token},
json: {type, mime, name, body, thumbnail}
}, (error, response) => {
@@ -62,7 +62,7 @@ const deleteBackpackObject = ({
}) => new Promise((resolve, reject) => {
xhr({
method: 'DELETE',
- uri: `${host}${username}/${id}`,
+ uri: `${host}/${username}/${id}`,
headers: {'x-token': token}
}, (error, response) => {
if (error || response.statusCode !== 200) {
diff --git a/src/lib/default-project/index.js b/src/lib/default-project/index.js
index 332e1bd98de..df1154b6e8a 100644
--- a/src/lib/default-project/index.js
+++ b/src/lib/default-project/index.js
@@ -1,5 +1,5 @@
import {TextEncoder} from 'text-encoding';
-import projectJson from './project.json';
+import projectData from './project-data';
/* eslint-disable import/no-unresolved */
import popWav from '!arraybuffer-loader!./83a9787d4cb6f3b7632b4ddfebf74367.wav';
@@ -10,34 +10,39 @@ import costume2 from '!raw-loader!./3696356a03a8d938318876a593572843.svg';
/* eslint-enable import/no-unresolved */
const encoder = new TextEncoder();
-export default [{
- id: 0,
- assetType: 'Project',
- dataFormat: 'JSON',
- data: JSON.stringify(projectJson)
-}, {
- id: '83a9787d4cb6f3b7632b4ddfebf74367',
- assetType: 'Sound',
- dataFormat: 'WAV',
- data: new Uint8Array(popWav)
-}, {
- id: '83c36d806dc92327b9e7049a565c6bff',
- assetType: 'Sound',
- dataFormat: 'WAV',
- data: new Uint8Array(meowWav)
-}, {
- id: 'cd21514d0531fdffb22204e0ec5ed84a',
- assetType: 'ImageVector',
- dataFormat: 'SVG',
- data: encoder.encode(backdrop)
-}, {
- id: '09dc888b0b7df19f70d81588ae73420e',
- assetType: 'ImageVector',
- dataFormat: 'SVG',
- data: encoder.encode(costume1)
-}, {
- id: '3696356a03a8d938318876a593572843',
- assetType: 'ImageVector',
- dataFormat: 'SVG',
- data: encoder.encode(costume2)
-}];
+const defaultProject = translator => {
+ const projectJson = projectData(translator);
+ return [{
+ id: 0,
+ assetType: 'Project',
+ dataFormat: 'JSON',
+ data: JSON.stringify(projectJson)
+ }, {
+ id: '83a9787d4cb6f3b7632b4ddfebf74367',
+ assetType: 'Sound',
+ dataFormat: 'WAV',
+ data: new Uint8Array(popWav)
+ }, {
+ id: '83c36d806dc92327b9e7049a565c6bff',
+ assetType: 'Sound',
+ dataFormat: 'WAV',
+ data: new Uint8Array(meowWav)
+ }, {
+ id: 'cd21514d0531fdffb22204e0ec5ed84a',
+ assetType: 'ImageVector',
+ dataFormat: 'SVG',
+ data: encoder.encode(backdrop)
+ }, {
+ id: '09dc888b0b7df19f70d81588ae73420e',
+ assetType: 'ImageVector',
+ dataFormat: 'SVG',
+ data: encoder.encode(costume1)
+ }, {
+ id: '3696356a03a8d938318876a593572843',
+ assetType: 'ImageVector',
+ dataFormat: 'SVG',
+ data: encoder.encode(costume2)
+ }];
+};
+
+export default defaultProject;
diff --git a/src/lib/default-project/project-data.js b/src/lib/default-project/project-data.js
new file mode 100644
index 00000000000..e108eaa17ed
--- /dev/null
+++ b/src/lib/default-project/project-data.js
@@ -0,0 +1,128 @@
+import {defineMessages} from 'react-intl';
+import sharedMessages from '../shared-messages';
+
+let messages = defineMessages({
+ meow: {
+ defaultMessage: 'Meow',
+ description: 'Name for the meow sound',
+ id: 'gui.defaultProject.meow'
+ },
+ variable: {
+ defaultMessage: 'my variable',
+ description: 'Name for the default variable',
+ id: 'gui.defaultProject.variable'
+ }
+});
+
+messages = {...messages, ...sharedMessages};
+
+// use the default message if a translation function is not passed
+const defaultTranslator = msgObj => msgObj.defaultMessage;
+
+/**
+ * Generate a localized version of the default project
+ * @param {function} translateFunction a function to use for translating the default names
+ * @return {object} the project data json for the default project
+ */
+const projectData = translateFunction => {
+ const translator = translateFunction || defaultTranslator;
+ return ({
+ targets: [
+ {
+ isStage: true,
+ name: 'Stage',
+ variables: {
+ '`jEk@4|i[#Fk?(8x)AV.-my variable': [
+ translator(messages.variable),
+ 0
+ ]
+ },
+ lists: {},
+ broadcasts: {},
+ blocks: {},
+ currentCostume: 0,
+ costumes: [
+ {
+ assetId: 'cd21514d0531fdffb22204e0ec5ed84a',
+ name: translator(messages.backdrop, {index: 1}),
+ md5ext: 'cd21514d0531fdffb22204e0ec5ed84a.svg',
+ dataFormat: 'svg',
+ rotationCenterX: 240,
+ rotationCenterY: 180
+ }
+ ],
+ sounds: [
+ {
+ assetId: '83a9787d4cb6f3b7632b4ddfebf74367',
+ name: translator(messages.pop),
+ dataFormat: 'wav',
+ format: '',
+ rate: 11025,
+ sampleCount: 258,
+ md5ext: '83a9787d4cb6f3b7632b4ddfebf74367.wav'
+ }
+ ],
+ volume: 100,
+ tempo: 60,
+ videoTransparency: 50,
+ videoState: 'off'
+ },
+ {
+ isStage: false,
+ name: translator(messages.sprite, {index: 1}),
+ variables: {},
+ lists: {},
+ broadcasts: {},
+ blocks: {},
+ currentCostume: 0,
+ costumes: [
+ {
+ assetId: '09dc888b0b7df19f70d81588ae73420e',
+ name: translator(messages.costume, {index: 1}),
+ bitmapResolution: 1,
+ md5ext: '09dc888b0b7df19f70d81588ae73420e.svg',
+ dataFormat: 'svg',
+ rotationCenterX: 47,
+ rotationCenterY: 55
+ },
+ {
+ assetId: '3696356a03a8d938318876a593572843',
+ name: translator(messages.costume, {index: 2}),
+ bitmapResolution: 1,
+ md5ext: '3696356a03a8d938318876a593572843.svg',
+ dataFormat: 'svg',
+ rotationCenterX: 47,
+ rotationCenterY: 55
+ }
+ ],
+ sounds: [
+ {
+ assetId: '83c36d806dc92327b9e7049a565c6bff',
+ name: translator(messages.meow),
+ dataFormat: 'wav',
+ format: '',
+ rate: 22050,
+ sampleCount: 18688,
+ md5ext: '83c36d806dc92327b9e7049a565c6bff.wav'
+ }
+ ],
+ volume: 100,
+ visible: true,
+ x: 0,
+ y: 0,
+ size: 100,
+ direction: 90,
+ draggable: false,
+ rotationStyle: 'all around'
+ }
+ ],
+ meta: {
+ semver: '3.0.0',
+ vm: '0.1.0',
+ agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36' // eslint-disable-line max-len
+ }
+ });
+};
+
+
+export default projectData;
diff --git a/src/lib/default-project/project.json b/src/lib/default-project/project.json
deleted file mode 100755
index 6ffa9fa3c71..00000000000
--- a/src/lib/default-project/project.json
+++ /dev/null
@@ -1,96 +0,0 @@
-{
- "targets": [
- {
- "isStage": true,
- "name": "Stage",
- "variables": {
- "`jEk@4|i[#Fk?(8x)AV.-my variable": [
- "my variable",
- 0
- ]
- },
- "lists": {},
- "broadcasts": {},
- "blocks": {},
- "currentCostume": 0,
- "costumes": [
- {
- "assetId": "cd21514d0531fdffb22204e0ec5ed84a",
- "name": "backdrop1",
- "md5ext": "cd21514d0531fdffb22204e0ec5ed84a.svg",
- "dataFormat": "svg",
- "rotationCenterX": 240,
- "rotationCenterY": 180
- }
- ],
- "sounds": [
- {
- "assetId": "83a9787d4cb6f3b7632b4ddfebf74367",
- "name": "pop",
- "dataFormat": "wav",
- "format": "",
- "rate": 11025,
- "sampleCount": 258,
- "md5ext": "83a9787d4cb6f3b7632b4ddfebf74367.wav"
- }
- ],
- "volume": 100,
- "tempo": 60,
- "videoTransparency": 50,
- "videoState": "off"
- },
- {
- "isStage": false,
- "name": "Sprite1",
- "variables": {},
- "lists": {},
- "broadcasts": {},
- "blocks": {},
- "currentCostume": 0,
- "costumes": [
- {
- "assetId": "09dc888b0b7df19f70d81588ae73420e",
- "name": "costume1",
- "bitmapResolution": 1,
- "md5ext": "09dc888b0b7df19f70d81588ae73420e.svg",
- "dataFormat": "svg",
- "rotationCenterX": 47,
- "rotationCenterY": 55
- },
- {
- "assetId": "3696356a03a8d938318876a593572843",
- "name": "costume2",
- "bitmapResolution": 1,
- "md5ext": "3696356a03a8d938318876a593572843.svg",
- "dataFormat": "svg",
- "rotationCenterX": 47,
- "rotationCenterY": 55
- }
- ],
- "sounds": [
- {
- "assetId": "83c36d806dc92327b9e7049a565c6bff",
- "name": "Meow",
- "dataFormat": "wav",
- "format": "",
- "rate": 22050,
- "sampleCount": 18688,
- "md5ext": "83c36d806dc92327b9e7049a565c6bff.wav"
- }
- ],
- "volume": 100,
- "visible": true,
- "x": 0,
- "y": 0,
- "size": 100,
- "direction": 90,
- "draggable": false,
- "rotationStyle": "all around"
- }
- ],
- "meta": {
- "semver": "3.0.0",
- "vm": "0.1.0",
- "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
- }
-}
diff --git a/src/lib/drop-area-hoc.jsx b/src/lib/drop-area-hoc.jsx
new file mode 100644
index 00000000000..9f44293e88c
--- /dev/null
+++ b/src/lib/drop-area-hoc.jsx
@@ -0,0 +1,88 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import omit from 'lodash.omit';
+import {connect} from 'react-redux';
+
+const DropAreaHOC = function (dragTypes) {
+ return function (WrappedComponent) {
+ class DropAreaWrapper extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'setRef'
+ ]);
+
+ this.state = {
+ dragOver: false
+ };
+
+ this.ref = null;
+ this.containerBox = null;
+ }
+
+ componentWillReceiveProps (newProps) {
+ // If `dragging` becomes true, record the drop area rectangle
+ if (newProps.dragInfo.dragging && !this.props.dragInfo.dragging) {
+ this.dropAreaRect = this.ref && this.ref.getBoundingClientRect();
+ // If `dragging` becomes false, call the drop handler
+ } else if (!newProps.dragInfo.dragging && this.props.dragInfo.dragging && this.state.dragOver) {
+ this.props.onDrop(this.props.dragInfo);
+ this.setState({dragOver: false});
+ }
+
+ // If a drag is in progress (currentOffset) and it matches the relevant drag types,
+ // test if the drag is within the drop area rect and set the state accordingly.
+ if (this.dropAreaRect && newProps.dragInfo.currentOffset &&
+ dragTypes.includes(newProps.dragInfo.dragType)) {
+ const {x, y} = newProps.dragInfo.currentOffset;
+ const {top, right, bottom, left} = this.dropAreaRect;
+ if (x > left && x < right && y > top && y < bottom) {
+ this.setState({dragOver: true});
+ } else {
+ this.setState({dragOver: false});
+ }
+ }
+ }
+ setRef (el) {
+ this.ref = el;
+ }
+ render () {
+ const componentProps = omit(this.props, ['onDrop', 'dragInfo']);
+ return (
+
+ );
+ }
+ }
+
+ DropAreaWrapper.propTypes = {
+ dragInfo: PropTypes.shape({
+ currentOffset: PropTypes.shape({
+ x: PropTypes.number,
+ y: PropTypes.number
+ }),
+ dragType: PropTypes.string,
+ dragging: PropTypes.bool,
+ index: PropTypes.number
+ }),
+ onDrop: PropTypes.func
+ };
+
+ const mapStateToProps = state => ({
+ dragInfo: state.scratchGui.assetDrag
+ });
+
+ const mapDispatchToProps = () => ({});
+
+ return connect(
+ mapStateToProps,
+ mapDispatchToProps
+ )(DropAreaWrapper);
+ };
+};
+
+export default DropAreaHOC;
diff --git a/src/lib/empty-assets.js b/src/lib/empty-assets.js
new file mode 100644
index 00000000000..65f1f678c5a
--- /dev/null
+++ b/src/lib/empty-assets.js
@@ -0,0 +1,64 @@
+/**
+ * @fileoverview
+ * Utility functions to return json corresponding to default empty assets.
+ */
+
+/**
+ * Generate a blank costume object for vm.addCostume with the provided name.
+ * @param {string} name the name to use for the costume, caller should localize
+ * @return {object} vm costume object
+ */
+const emptyCostume = name => ({
+ name: name,
+ md5: 'cd21514d0531fdffb22204e0ec5ed84a.svg',
+ rotationCenterX: 0,
+ rotationCenterY: 0,
+ bitmapResolution: 1,
+ skinId: null
+});
+
+/**
+ * Generate a new empty sprite. The caller should provide localized versions of the
+ * default names.
+ * @param {string} name the name to use for the sprite
+ * @param {string} soundName the name to use for the default sound
+ * @param {string} costumeName the name to use for the default costume
+ * @return {object} object expected by vm.addSprite
+ */
+const emptySprite = (name, soundName, costumeName) => ({
+ objName: name,
+ sounds: [
+ {
+ soundName: soundName,
+ soundID: -1,
+ md5: '83a9787d4cb6f3b7632b4ddfebf74367.wav',
+ sampleCount: 258,
+ rate: 11025,
+ format: ''
+ }
+ ],
+ costumes: [
+ {
+ costumeName: costumeName,
+ baseLayerID: -1,
+ baseLayerMD5: 'cd21514d0531fdffb22204e0ec5ed84a.svg',
+ bitmapResolution: 1,
+ rotationCenterX: 0,
+ rotationCenterY: 0
+ }
+ ],
+ currentCostumeIndex: 0,
+ scratchX: 36,
+ scratchY: 28,
+ scale: 1,
+ direction: 90,
+ rotationStyle: 'normal',
+ isDraggable: false,
+ visible: true,
+ spriteInfo: {}
+});
+
+export {
+ emptyCostume,
+ emptySprite
+};
diff --git a/src/lib/file-uploader.js b/src/lib/file-uploader.js
index 53d0ab8f651..afa4371dde1 100644
--- a/src/lib/file-uploader.js
+++ b/src/lib/file-uploader.js
@@ -180,7 +180,8 @@ const soundUpload = function (fileData, fileType, soundName, storage, handleSoun
handleSound(vmSound);
};
-const spriteUpload = function (fileData, fileType, spriteName, storage, handleSprite) {
+const spriteUpload = function (fileData, fileType, spriteName, storage, handleSprite, costumeSuffix) {
+ const costumeName = costumeSuffix || 'costume1';
switch (fileType) {
case '':
case 'application/zip': { // We think this is a .sprite2 or .sprite3 file
@@ -191,7 +192,7 @@ const spriteUpload = function (fileData, fileType, spriteName, storage, handleSp
case 'image/png':
case 'image/jpeg': {
// Make a sprite from an image by making it a costume first
- costumeUpload(fileData, fileType, `${spriteName}-costume1`, storage, (vmCostume => {
+ costumeUpload(fileData, fileType, `${spriteName}-${costumeName}`, storage, (vmCostume => {
const newSprite = {
name: spriteName,
isStage: false,
diff --git a/src/lib/hash-parser-hoc.jsx b/src/lib/hash-parser-hoc.jsx
index f3ac9254280..b5dedeeac0e 100644
--- a/src/lib/hash-parser-hoc.jsx
+++ b/src/lib/hash-parser-hoc.jsx
@@ -33,6 +33,7 @@ const HashParserHOC = function (WrappedComponent) {
render () {
return (
diff --git a/src/lib/libraries/backdrop-tags.js b/src/lib/libraries/backdrop-tags.js
index 7bafa3410aa..77374719d2b 100644
--- a/src/lib/libraries/backdrop-tags.js
+++ b/src/lib/libraries/backdrop-tags.js
@@ -1,10 +1,11 @@
+import messages from './tag-messages.js';
export default [
- {title: 'Fantasy'},
- {title: 'Music'},
- {title: 'Sports'},
- {title: 'Outdoors'},
- {title: 'Indoors'},
- {title: 'Space'},
- {title: 'Underwater'},
- {title: 'Patterns'}
+ {tag: 'fantasy', intlLabel: messages.fantasy},
+ {tag: 'music', intlLabel: messages.music},
+ {tag: 'sports', intlLabel: messages.sports},
+ {tag: 'outdoors', intlLabel: messages.outdoors},
+ {tag: 'indoors', intlLabel: messages.indoors},
+ {tag: 'space', intlLabel: messages.space},
+ {tag: 'underwater', intlLabel: messages.underwater},
+ {tag: 'patterns', intlLabel: messages.patterns}
];
diff --git a/src/lib/libraries/backdrops.json b/src/lib/libraries/backdrops.json
index 14f82655b64..91e9b1cf489 100644
--- a/src/lib/libraries/backdrops.json
+++ b/src/lib/libraries/backdrops.json
@@ -442,7 +442,7 @@
]
},
{
- "name": "Jurrasic",
+ "name": "Jurassic",
"md5": "64025bdca5db4938f65597e3682fddcf.svg",
"type": "backdrop",
"tags": [
diff --git a/src/lib/libraries/costumes.json b/src/lib/libraries/costumes.json
index 88cd587e7ae..95bab89bb4a 100644
--- a/src/lib/libraries/costumes.json
+++ b/src/lib/libraries/costumes.json
@@ -73,6 +73,82 @@
2
]
},
+ {
+ "name": "Andie-a",
+ "md5": "936abdaaac3a3f7d27e5f30b3da00e5f.svg",
+ "type": "costume",
+ "tags": [
+ "sports",
+ "basketball",
+ "people",
+ "wheelchair",
+ "handicap",
+ "handicapable",
+ "alex eben meyer"
+ ],
+ "info": [
+ 86,
+ 77,
+ 1
+ ]
+ },
+ {
+ "name": "Andie-b",
+ "md5": "cc2953492b40e231fdee3d8c9994a646.svg",
+ "type": "costume",
+ "tags": [
+ "sports",
+ "basketball",
+ "people",
+ "wheelchair",
+ "handicap",
+ "handicapable",
+ "alex eben meyer"
+ ],
+ "info": [
+ 46,
+ 77,
+ 1
+ ]
+ },
+ {
+ "name": "Andie-c",
+ "md5": "76e26ccb79a94d3e89f67e64b4cb9731.svg",
+ "type": "costume",
+ "tags": [
+ "sports",
+ "basketball",
+ "people",
+ "wheelchair",
+ "handicap",
+ "handicapable",
+ "alex eben meyer"
+ ],
+ "info": [
+ 52,
+ 36,
+ 1
+ ]
+ },
+ {
+ "name": "Andie-d",
+ "md5": "80fa011244f3e3be56b96ebcb9e40d59.svg",
+ "type": "costume",
+ "tags": [
+ "sports",
+ "basketball",
+ "people",
+ "wheelchair",
+ "handicap",
+ "handicapable",
+ "alex eben meyer"
+ ],
+ "info": [
+ 86,
+ 43,
+ 1
+ ]
+ },
{
"name": "Anina Pop Down",
"md5": "e3698b76cb0864df2fbaba80e6bd8067.png",
@@ -89,7 +165,7 @@
},
{
"name": "Anina Pop Front",
- "md5": "832a3ae650a1f3bede1e0c9f1779e535.png",
+ "md5": "4931a363e3e4efa20230f6ff2991c6b4.png",
"type": "costume",
"tags": [
"people",
@@ -131,7 +207,7 @@
},
{
"name": "Anina Pop R Arm",
- "md5": "eac2b5f7db5e81d00c43486179b8dcf8.png",
+ "md5": "ca27e001a263ee6b5852508f39d021db.png",
"type": "costume",
"tags": [
"people",
@@ -173,7 +249,7 @@
},
{
"name": "Anina R Cross",
- "md5": "f0d4dce1006cf447a0a4b53e0a8d0519.png",
+ "md5": "3948aad16f8169c013c956dd152a09a6.png",
"type": "costume",
"tags": [
"people",
@@ -187,7 +263,7 @@
},
{
"name": "Anina Stance",
- "md5": "9374e067ca4b66427fc64a0822f6e468.png",
+ "md5": "b66762519d9542c0ab4fe754b47fe593.png",
"type": "costume",
"tags": [
"people",
@@ -201,7 +277,7 @@
},
{
"name": "Anina Top Freeze",
- "md5": "a1c9b65dd07747964cce642f9099921b.png",
+ "md5": "b7693bd6250d4411ee622b67f8025924.png",
"type": "costume",
"tags": [
"people",
@@ -659,7 +735,7 @@
},
{
"name": "Bat-a",
- "md5": "a68f7a0b2af7514c729bb083d04dbbde.svg",
+ "md5": "3b6274510488d5b26447c1c266475801.svg",
"type": "costume",
"tags": [
"fantasy",
@@ -670,14 +746,14 @@
"alex eben meyer"
],
"info": [
- 51,
- 47,
+ 65,
+ 61,
1
]
},
{
"name": "Bat-b",
- "md5": "4ea5bb61370e189d2eba763312680201.svg",
+ "md5": "8b20987d450379a445cbfc41ff2a3874.svg",
"type": "costume",
"tags": [
"fantasy",
@@ -688,14 +764,14 @@
"alex eben meyer"
],
"info": [
- 51,
- 47,
+ 65,
+ 61,
1
]
},
{
"name": "Bat-c",
- "md5": "9ed4deead22db511202f38abadf508c4.svg",
+ "md5": "c6d6a6892638e489902fbba803050de1.svg",
"type": "costume",
"tags": [
"fantasy",
@@ -706,14 +782,14 @@
"alex eben meyer"
],
"info": [
- 51,
- 47,
+ 65,
+ 61,
1
]
},
{
"name": "Bat-d",
- "md5": "293a7b03badd2f3b49e50acfb9c242ec.svg",
+ "md5": "e3da64838b2c74b6dd9a080b030e327e.svg",
"type": "costume",
"tags": [
"fantasy",
@@ -724,8 +800,8 @@
"alex eben meyer"
],
"info": [
- 51,
- 47,
+ 65,
+ 61,
1
]
},
@@ -1073,6 +1149,19 @@
1
]
},
+ {
+ "name": "Bowtie-01",
+ "md5": "aebd82272a5f4a1dd3c23d6e98537017.svg",
+ "type": "costume",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 13,
+ 7,
+ 1
+ ]
+ },
{
"name": "Bowtie-a",
"md5": "534d9924d2f9bfe240f041e3ce55ccf5.svg",
@@ -1698,26 +1787,23 @@
]
},
{
- "name": "Cat1",
- "md5": "09dc888b0b7df19f70d81588ae73420e.svg",
+ "name": "Cat 2",
+ "md5": "01ae57fd339529445cb890978ef8a054.svg",
"type": "costume",
"tags": [
- "animals",
- "cat",
- "kitten",
"kitty",
- "mammal",
- "orange",
- "scratch cat"
+ "kitten",
+ "animals",
+ "mammal"
],
"info": [
- 47,
- 55,
+ 87,
+ 39,
1
]
},
{
- "name": "Cat1 Flying-a",
+ "name": "Cat Flying-a",
"md5": "1e81725d2d2c7de4a2dd4a145198a43c.svg",
"type": "costume",
"tags": [
@@ -1733,7 +1819,7 @@
]
},
{
- "name": "Cat1 Flying-b",
+ "name": "Cat Flying-b",
"md5": "0d192725870ef0eda50d91cab0e3c9c5.svg",
"type": "costume",
"tags": [
@@ -1750,7 +1836,26 @@
]
},
{
- "name": "Cat2",
+ "name": "Cat-a",
+ "md5": "09dc888b0b7df19f70d81588ae73420e.svg",
+ "type": "costume",
+ "tags": [
+ "animals",
+ "cat",
+ "kitten",
+ "kitty",
+ "mammal",
+ "orange",
+ "scratch cat"
+ ],
+ "info": [
+ 47,
+ 55,
+ 1
+ ]
+ },
+ {
+ "name": "Cat-b",
"md5": "3696356a03a8d938318876a593572843.svg",
"type": "costume",
"tags": [
@@ -1898,7 +2003,7 @@
},
{
"name": "Champ99-a",
- "md5": "9e4e87818854cfc281d8dcc3e21ee672.png",
+ "md5": "1cb9d1a98cebc8a1d83af7633fdb9cd0.png",
"type": "costume",
"tags": [
"people",
@@ -2839,16 +2944,16 @@
},
{
"name": "Dm Stance",
- "md5": "b30a60d31aebead577b73affd22bf30f.svg",
+ "md5": "dafbdfe454c5ec7029b5c1e07fcabc90.png",
"type": "costume",
"tags": [
"people",
"dance"
],
"info": [
- 55,
- 119,
- 1
+ 212,
+ 476,
+ 2
]
},
{
@@ -2867,7 +2972,7 @@
},
{
"name": "Dm Top R Leg",
- "md5": "6cea0c29c460e571226cc4cff5b97c28.png",
+ "md5": "12db59633a1709a2c39534d35263791f.png",
"type": "costume",
"tags": [
"people",
@@ -2892,7 +2997,7 @@
},
{
"name": "Dm Top Stand",
- "md5": "051c36758b9fd1136b511efc5dd6234a.png",
+ "md5": "a22da98e5e63de7b2883355afd0184f0.png",
"type": "costume",
"tags": [
"people",
@@ -3238,6 +3343,45 @@
1
]
},
+ {
+ "name": "Dress-01",
+ "md5": "308b6ad8e6cfe5c2b2c1e3a1eb9cc404.svg",
+ "type": "costume",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 58,
+ 83,
+ 1
+ ]
+ },
+ {
+ "name": "Dress-02",
+ "md5": "1f7a725ba285dc97cb142d4b812db624.svg",
+ "type": "costume",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 49,
+ 83,
+ 1
+ ]
+ },
+ {
+ "name": "Dress-03",
+ "md5": "fa7e0b37a324c212d0f1ce0f701a1ae0.svg",
+ "type": "costume",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 57,
+ 83,
+ 1
+ ]
+ },
{
"name": "Drum-a",
"md5": "dd66742bc2a3cfe5a6f9f540afd2e15c.svg",
@@ -4261,6 +4405,62 @@
1
]
},
+ {
+ "name": "Glasses-a",
+ "md5": "f42180039c37c3b221e1e426b92e5092.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "glasses"
+ ],
+ "info": [
+ 33,
+ 13,
+ 1
+ ]
+ },
+ {
+ "name": "Glasses-b",
+ "md5": "58b728ee8c1f739e2de13800dee562de.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "glasses"
+ ],
+ "info": [
+ 35,
+ 14,
+ 1
+ ]
+ },
+ {
+ "name": "Glasses-c",
+ "md5": "d8bf373e1fcbb46f6eb8d225ba01613e.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "glasses"
+ ],
+ "info": [
+ 34,
+ 12,
+ 1
+ ]
+ },
+ {
+ "name": "Glasses-e",
+ "md5": "6b706718a8c350522c3f29fb82b40327.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "glasses"
+ ],
+ "info": [
+ 39,
+ 53,
+ 1
+ ]
+ },
{
"name": "Goalie-a",
"md5": "86b0610ea21fdecb99795c5e6d52768c.svg",
@@ -4702,6 +4902,48 @@
1
]
},
+ {
+ "name": "Harper-a",
+ "md5": "919eb332bf73df227ec5f214be952a21.svg",
+ "type": "costume",
+ "tags": [
+ "people",
+ "fashion"
+ ],
+ "info": [
+ 54,
+ 135,
+ 1
+ ]
+ },
+ {
+ "name": "Harper-b",
+ "md5": "0d5979db40b0a4dcaa601fb7679ef71c.svg",
+ "type": "costume",
+ "tags": [
+ "people",
+ "fashion"
+ ],
+ "info": [
+ 54,
+ 143,
+ 1
+ ]
+ },
+ {
+ "name": "Harper-c",
+ "md5": "9c35763ec87604ff1b4848ff18e0b040.svg",
+ "type": "costume",
+ "tags": [
+ "people",
+ "fashion"
+ ],
+ "info": [
+ 56,
+ 138,
+ 1
+ ]
+ },
{
"name": "Hat",
"md5": "b3beb1f52d371428d70b65a0c4c5c001.svg",
@@ -4767,6 +5009,62 @@
1
]
},
+ {
+ "name": "Hat-a",
+ "md5": "874462b3e079308d775629c235940d8f.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "hat"
+ ],
+ "info": [
+ 39,
+ 22,
+ 1
+ ]
+ },
+ {
+ "name": "Hat-b",
+ "md5": "a8ef874bd7542fec62785eac80019417.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "hat"
+ ],
+ "info": [
+ 45,
+ 30,
+ 1
+ ]
+ },
+ {
+ "name": "Hat-c",
+ "md5": "52540f9dba4537f79f1ad3067d44e3f6.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "hat"
+ ],
+ "info": [
+ 37,
+ 30,
+ 1
+ ]
+ },
+ {
+ "name": "Hat-d",
+ "md5": "b40accecd4bd57fed8bdb96ee15e5c05.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "hat"
+ ],
+ "info": [
+ 40,
+ 27,
+ 1
+ ]
+ },
{
"name": "Hatchling-a",
"md5": "fb9de507b6d95e96cb0df68e401dfd8f.svg",
@@ -5254,7 +5552,7 @@
]
},
{
- "name": "Jamie-a",
+ "name": "Jamal-a",
"md5": "90f9166fe6500d0c0caad8b1964d6b74.svg",
"type": "costume",
"tags": [
@@ -5270,7 +5568,7 @@
]
},
{
- "name": "Jamie-b",
+ "name": "Jamal-b",
"md5": "c3d96ef7e99440c2fa76effce1235d3f.svg",
"type": "costume",
"tags": [
@@ -5286,7 +5584,7 @@
]
},
{
- "name": "Jamie-c",
+ "name": "Jamal-c",
"md5": "1fb8b9ca79f2c0a327913bd647b53fe5.svg",
"type": "costume",
"tags": [
@@ -5302,7 +5600,7 @@
]
},
{
- "name": "Jamie-d",
+ "name": "Jamal-d",
"md5": "4adb87e6123161fcaf02f7ac022a5757.svg",
"type": "costume",
"tags": [
@@ -5496,7 +5794,7 @@
},
{
"name": "Jo Pop R Arm",
- "md5": "a35a3bbc4e21ce2f5de940adb7017fa6.png",
+ "md5": "aabfedff0d11243386b6b0941e0f72e9.png",
"type": "costume",
"tags": [
"people",
@@ -5538,7 +5836,7 @@
},
{
"name": "Jo Stance",
- "md5": "e10d129d9be2147bc7d284e281d88e9a.png",
+ "md5": "6f68790ee3eb9bdccf8749305186b0dd.png",
"type": "costume",
"tags": [
"people",
@@ -6019,7 +6317,7 @@
},
{
"name": "Lb Stance",
- "md5": "9d64910e84c39660b19ca4ca8b45e2be.png",
+ "md5": "71dde8c43985815bffb5a5ed5632af58.png",
"type": "costume",
"tags": [
"people",
@@ -7025,6 +7323,34 @@
1
]
},
+ {
+ "name": "Pants-a",
+ "md5": "f7a0584eca14eaa0e29662e5c24fb62d.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "pants"
+ ],
+ "info": [
+ 34,
+ 66,
+ 1
+ ]
+ },
+ {
+ "name": "Pants-b",
+ "md5": "22ea0cf68b885817a5b6a01194c6f5d3.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "pants"
+ ],
+ "info": [
+ 35,
+ 66,
+ 1
+ ]
+ },
{
"name": "Parrot-a",
"md5": "098570b8e1aa85b32f9b4eb07bea3af2.svg",
@@ -7675,6 +8001,32 @@
1
]
},
+ {
+ "name": "Pumpkin-costume",
+ "md5": "6f7a5a88e6201082913a099438d626ef.svg",
+ "type": "costume",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 39,
+ 63,
+ 1
+ ]
+ },
+ {
+ "name": "Pumpkin-hat",
+ "md5": "22bdeaf0d8421ffb70775ed77070bee7.svg",
+ "type": "costume",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 28,
+ 24,
+ 1
+ ]
+ },
{
"name": "Puppy Back",
"md5": "05630bfa94501a3e5d61ce443a0cea70.png",
@@ -8453,6 +8805,20 @@
1
]
},
+ {
+ "name": "Shirt-a",
+ "md5": "659465944053fe6fb6aa1ed0e11be9aa.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "shirt"
+ ],
+ "info": [
+ 46,
+ 40,
+ 1
+ ]
+ },
{
"name": "Shirt-t",
"md5": "5d7fa4f1788f03d2962f1624d6eac800.svg",
@@ -8492,6 +8858,62 @@
1
]
},
+ {
+ "name": "Shoes-a",
+ "md5": "4639a1af5bc91f1a6f14e822cd46972f.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "shoes"
+ ],
+ "info": [
+ 40,
+ 13,
+ 1
+ ]
+ },
+ {
+ "name": "Shoes-b",
+ "md5": "6ba2a692c17f47170d611578a5620ae5.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "shoes"
+ ],
+ "info": [
+ 40,
+ 31,
+ 1
+ ]
+ },
+ {
+ "name": "Shoes-c",
+ "md5": "14c6843195bd13824d253cb3fdb9e6de.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "shoes"
+ ],
+ "info": [
+ 45,
+ 33,
+ 1
+ ]
+ },
+ {
+ "name": "Shoes-d",
+ "md5": "1130f7ca93716bac6afa31af86329e92.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "shoes"
+ ],
+ "info": [
+ 44,
+ 32,
+ 1
+ ]
+ },
{
"name": "Shoes1",
"md5": "ffab4cc284070b50ac317e515f59f7d8.svg",
@@ -8518,6 +8940,54 @@
1
]
},
+ {
+ "name": "Shorts-a",
+ "md5": "d9b580c913c0d1d1a996dd733af91e68.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "pants",
+ "shorts",
+ "clothing"
+ ],
+ "info": [
+ 35,
+ 37,
+ 1
+ ]
+ },
+ {
+ "name": "Shorts-b",
+ "md5": "1e07ab2763e5e0f5557f97c0e2c89020.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ "pants",
+ "shorts",
+ "clothing"
+ ],
+ "info": [
+ 43,
+ 36,
+ 1
+ ]
+ },
+ {
+ "name": "Shorts-c",
+ "md5": "dc9e0a1c2489ec0bf59a937b6c8ae85b.svg",
+ "type": "costume",
+ "tags": [
+ "fashion",
+ " pants",
+ "shorts",
+ "clothing"
+ ],
+ "info": [
+ 35,
+ 29,
+ 1
+ ]
+ },
{
"name": "Singer1",
"md5": "e47ef1af3b925e5ac9e3b3f809d440b3.svg",
@@ -8941,34 +9411,33 @@
]
},
{
- "name": "Taco-a",
- "md5": "d4a2cea411c6030f017f25184562559d.svg",
+ "name": "Taco",
+ "md5": "bc78fb90ed373d56c11d5fafa4203ccd.svg",
"type": "costume",
"tags": [
"food",
- "waffle",
- "yum",
- "delicious"
+ "designerd"
],
"info": [
- 19,
- 15,
+ 78,
+ 48,
1
]
},
{
- "name": "Taco-b",
- "md5": "cef6f6d7c92fc1f008077319ac12dc5e.svg",
+ "name": "Taco-wizard",
+ "md5": "5e9e65db20d403b590578ed44b1a3792.svg",
"type": "costume",
"tags": [
"food",
- "waffle",
- "yum",
- "delicious"
+ "fantasy",
+ "abrakadabra",
+ "alakazam",
+ "designerd"
],
"info": [
- 56,
- 15,
+ 125,
+ 82,
1
]
},
@@ -9044,7 +9513,7 @@
},
{
"name": "Ten80 Pop Down",
- "md5": "14685e432d683c918328e7fb070b7029.png",
+ "md5": "fea7045c09073700b88fae8d4d257cd1.png",
"type": "costume",
"tags": [
"people",
@@ -9198,7 +9667,7 @@
},
{
"name": "Ten80 Top R Step",
- "md5": "733d05c17699758e2723599de333fdc0.png",
+ "md5": "580fba92f23d5592200eb5a9079dc38f.png",
"type": "costume",
"tags": [
"people",
diff --git a/src/lib/libraries/decks/animate/lib_animate-a-name.jpg b/src/lib/libraries/decks/animate/lib_animate-a-name.jpg
index 139f3f98662..391eb8971e8 100644
Binary files a/src/lib/libraries/decks/animate/lib_animate-a-name.jpg and b/src/lib/libraries/decks/animate/lib_animate-a-name.jpg differ
diff --git a/src/lib/libraries/decks/chase-game/lib-chasegame.jpg b/src/lib/libraries/decks/chase-game/lib-chasegame.jpg
index ec9861467d7..6e74c212872 100644
Binary files a/src/lib/libraries/decks/chase-game/lib-chasegame.jpg and b/src/lib/libraries/decks/chase-game/lib-chasegame.jpg differ
diff --git a/src/lib/libraries/decks/game/lib-pop.jpg b/src/lib/libraries/decks/game/lib-pop.jpg
index 4392196fe46..dfe78f63778 100644
Binary files a/src/lib/libraries/decks/game/lib-pop.jpg and b/src/lib/libraries/decks/game/lib-pop.jpg differ
diff --git a/src/lib/libraries/decks/index.jsx b/src/lib/libraries/decks/index.jsx
index 941ec9c8ff1..f8324f3ae73 100644
--- a/src/lib/libraries/decks/index.jsx
+++ b/src/lib/libraries/decks/index.jsx
@@ -42,6 +42,10 @@ import stepScoreWhenTouch from './chase-game/chase-game-change-score.gif';
// Make-A-Game
import libraryMakeAGame from './game/lib-pop.jpg';
+import stepGamePickSprite from './game/game-pick-sprite.gif';
+import stepGamePlaySound from './game/game-play-sound.gif';
+import stepGameAddScore from './game/game-add-score.gif';
+import stepGameChangeScore from './game/game-change-score.gif';
import stepRandom from './game/game-random-position.gif';
import stepGameChangeColor from './game/game-change-color.gif';
import stepResetScore from './game/game-reset-score.gif';
@@ -70,7 +74,7 @@ export default {
img: libraryIntro,
steps: [{
- video: 'https://www.youtube.com/embed/4CexmCCREZw'
+ video: 'https://www.youtube.com/embed/h9x8IPGN3SI'
}, {
title: (
+ ),
+ image: stepGamePickSprite
+ }, {
+ title: (
+
+ ),
+ image: stepGamePlaySound
+ }, {
+ title: (
+
+ ),
+ image: stepGameAddScore
+ }, {
+ title: (
+
+ ),
+
+ image: stepGameChangeScore
+ }, {
title: (
),
image: stepRandom
@@ -266,7 +307,7 @@ export default {
),
image: stepGameChangeColor
@@ -282,11 +323,11 @@ export default {
}, {
deckIds: [
'add-a-backdrop',
- 'add-effects',
'move-around-with-arrow-keys'
]
}
- ]
+ ],
+ urlId: 4
},
'Chase-Game': {
@@ -299,7 +340,7 @@ export default {
),
img: libraryChaseGame,
steps: [{
- video: 'https://www.youtube.com/embed/PoQO35QmlVA'
+ video: 'https://www.youtube.com/embed/IRf9-P8PiZo'
},
{
title: (
@@ -384,12 +425,12 @@ export default {
image: stepScoreWhenTouch
}, {
deckIds: [
- 'add-a-backdrop',
'add-effects',
'move-around-with-arrow-keys'
]
}
- ]
+ ],
+ urlId: 5
},
'add-sprite': {
name: (
@@ -414,11 +455,11 @@ export default {
{
deckIds: [
'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'switch-costume'
]
}
- ]
+ ],
+ urlId: 6
},
'add-a-backdrop': {
name: (
@@ -430,14 +471,14 @@ export default {
),
img: addBackdropThumb,
steps: [{
- video: 'https://www.youtube.com/embed/WpV05Q7AbPU'
+ video: 'https://www.youtube.com/embed/Xv3Z80yy2l0'
}, {
deckIds: [
- 'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'change-size',
+ 'switch-costume'
]
- }]
+ }],
+ urlId: 7
},
'change-size': {
name: (
@@ -449,14 +490,14 @@ export default {
),
img: changeSizeThumb,
steps: [{
- video: 'https://www.youtube.com/embed/NiK9KcghZ9s'
+ video: 'https://www.youtube.com/embed/PJijGbhcT3E'
}, {
deckIds: [
- 'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'glide-around',
+ 'spin-video'
]
- }]
+ }],
+ urlId: 8
},
'glide-around': {
name: (
@@ -468,14 +509,14 @@ export default {
),
img: glideAroundThumb,
steps: [{
- video: 'https://www.youtube.com/embed/WUcmsMEIbGg'
+ video: 'https://www.youtube.com/embed/KYmbgLX1xDs'
}, {
deckIds: [
'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'switch-costume'
]
- }]
+ }],
+ urlId: 9
},
'record-a-sound': {
@@ -488,28 +529,33 @@ export default {
),
img: recordASound,
steps: [{
- video: 'https://www.youtube.com/embed/epZQpVdf884'
+ video: 'https://www.youtube.com/embed/1WaU6e70Zig'
}, {
deckIds: [
'Make-Music',
- 'switch-costume',
- 'change-size'
+ 'switch-costume'
]
- }]
-
+ }],
+ urlId: 10
},
'spin-video': {
- name: 'Make It Spin',
+ name: (
+
+ ),
img: spinThumb,
steps: [{
- video: 'https://www.youtube.com/embed/rHP3aojB_6w'
+ video: 'https://www.youtube.com/embed/C76V5cuI9XM'
}, {
deckIds: [
'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'switch-costume'
]
- }]
+ }],
+ urlId: 11
},
'hide-and-show': {
name: (
@@ -521,14 +567,14 @@ export default {
),
img: hideAndShowThumb,
steps: [{
- video: 'https://www.youtube.com/embed/jpvqnlfsDTU'
+ video: 'https://www.youtube.com/embed/6yWUvRU19ms'
}, {
deckIds: [
'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'switch-costume'
]
- }]
+ }],
+ urlId: 12
},
'switch-costume': {
@@ -541,28 +587,34 @@ export default {
),
img: switchCostumeThumb,
steps: [{
- video: 'https://www.youtube.com/embed/AUBoFxQDPWA'
+ video: 'https://www.youtube.com/embed/vppgw1Xiegw'
}, {
deckIds: [
'add-a-backdrop',
- 'add-effects',
- 'change-size'
+ 'add-effects'
]
- }]
+ }],
+ urlId: 13
},
'move-around-with-arrow-keys': {
- name: 'Use Arrow Keys',
+ name: (
+
+ ),
img: moveArrowKeysThumb,
steps: [{
- video: 'https://www.youtube.com/embed/7DUA_Yl0B_M'
+ video: 'https://www.youtube.com/embed/uf6agkKnXJw'
}, {
deckIds: [
'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'switch-costume'
]
- }]
+ }],
+ urlId: 14
},
'add-effects': {
name: (
@@ -574,13 +626,13 @@ export default {
),
img: addEffectsThumb,
steps: [{
- video: 'https://www.youtube.com/embed/ORuohhkx15g'
+ video: 'https://www.youtube.com/embed/w3kGWEzRtxY'
}, {
deckIds: [
'add-a-backdrop',
- 'switch-costume',
- 'change-size'
+ 'switch-costume'
]
- }]
+ }],
+ urlId: 15
}
};
diff --git a/src/lib/libraries/decks/intro/lib-getting-started.jpg b/src/lib/libraries/decks/intro/lib-getting-started.jpg
index fd081dbd36f..553e881d9a3 100644
Binary files a/src/lib/libraries/decks/intro/lib-getting-started.jpg and b/src/lib/libraries/decks/intro/lib-getting-started.jpg differ
diff --git a/src/lib/libraries/decks/make-music/lib-make-music.jpg b/src/lib/libraries/decks/make-music/lib-make-music.jpg
index 7fbde54be65..a00248320ac 100644
Binary files a/src/lib/libraries/decks/make-music/lib-make-music.jpg and b/src/lib/libraries/decks/make-music/lib-make-music.jpg differ
diff --git a/src/lib/libraries/decks/spin/change-color.gif b/src/lib/libraries/decks/spin/change-color.gif
deleted file mode 100644
index dca51dad4e1..00000000000
Binary files a/src/lib/libraries/decks/spin/change-color.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/spin/click-control.gif b/src/lib/libraries/decks/spin/click-control.gif
deleted file mode 100644
index 24128a0edd0..00000000000
Binary files a/src/lib/libraries/decks/spin/click-control.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/spin/click-forever.gif b/src/lib/libraries/decks/spin/click-forever.gif
deleted file mode 100644
index f9d465bc093..00000000000
Binary files a/src/lib/libraries/decks/spin/click-forever.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/spin/click-turn.gif b/src/lib/libraries/decks/spin/click-turn.gif
deleted file mode 100644
index af887244849..00000000000
Binary files a/src/lib/libraries/decks/spin/click-turn.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/spin/drag-forever.gif b/src/lib/libraries/decks/spin/drag-forever.gif
deleted file mode 100644
index a51d6b4385d..00000000000
Binary files a/src/lib/libraries/decks/spin/drag-forever.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/spin/drag-turn.gif b/src/lib/libraries/decks/spin/drag-turn.gif
deleted file mode 100644
index 8df663eaf93..00000000000
Binary files a/src/lib/libraries/decks/spin/drag-turn.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/spin/library-spin.gif b/src/lib/libraries/decks/spin/library-spin.gif
deleted file mode 100644
index 12e60ecf79b..00000000000
Binary files a/src/lib/libraries/decks/spin/library-spin.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/spin/thumb-spin.gif b/src/lib/libraries/decks/spin/thumb-spin.gif
deleted file mode 100644
index e151455be73..00000000000
Binary files a/src/lib/libraries/decks/spin/thumb-spin.gif and /dev/null differ
diff --git a/src/lib/libraries/decks/sprite/cover-add-sprite.jpg b/src/lib/libraries/decks/sprite/cover-add-sprite.jpg
index c0a0359e0c4..e7a13449072 100644
Binary files a/src/lib/libraries/decks/sprite/cover-add-sprite.jpg and b/src/lib/libraries/decks/sprite/cover-add-sprite.jpg differ
diff --git a/src/lib/libraries/decks/videos/add-backdrop.jpg b/src/lib/libraries/decks/videos/add-backdrop.jpg
index ec5062c82d9..f046afb70cc 100644
Binary files a/src/lib/libraries/decks/videos/add-backdrop.jpg and b/src/lib/libraries/decks/videos/add-backdrop.jpg differ
diff --git a/src/lib/libraries/decks/videos/add-effects.jpg b/src/lib/libraries/decks/videos/add-effects.jpg
index 2f266057c6f..bb773b2ea6d 100644
Binary files a/src/lib/libraries/decks/videos/add-effects.jpg and b/src/lib/libraries/decks/videos/add-effects.jpg differ
diff --git a/src/lib/libraries/decks/videos/animate-sprite.jpg b/src/lib/libraries/decks/videos/animate-sprite.jpg
index e6eb10d6701..b67c47bbb0f 100644
Binary files a/src/lib/libraries/decks/videos/animate-sprite.jpg and b/src/lib/libraries/decks/videos/animate-sprite.jpg differ
diff --git a/src/lib/libraries/decks/videos/change-size.jpg b/src/lib/libraries/decks/videos/change-size.jpg
index 6f391b78f0e..e496a7d81fe 100644
Binary files a/src/lib/libraries/decks/videos/change-size.jpg and b/src/lib/libraries/decks/videos/change-size.jpg differ
diff --git a/src/lib/libraries/decks/videos/glide-around.jpg b/src/lib/libraries/decks/videos/glide-around.jpg
index ee006c3bef0..b9e140c452e 100644
Binary files a/src/lib/libraries/decks/videos/glide-around.jpg and b/src/lib/libraries/decks/videos/glide-around.jpg differ
diff --git a/src/lib/libraries/decks/videos/hide-and-show.jpg b/src/lib/libraries/decks/videos/hide-and-show.jpg
index a5e561c61ae..564d6b958d2 100644
Binary files a/src/lib/libraries/decks/videos/hide-and-show.jpg and b/src/lib/libraries/decks/videos/hide-and-show.jpg differ
diff --git a/src/lib/libraries/decks/videos/move-arrow-keys.jpg b/src/lib/libraries/decks/videos/move-arrow-keys.jpg
index ada61ee67ad..6524b5d6c74 100644
Binary files a/src/lib/libraries/decks/videos/move-arrow-keys.jpg and b/src/lib/libraries/decks/videos/move-arrow-keys.jpg differ
diff --git a/src/lib/libraries/decks/videos/record-a-sound.jpg b/src/lib/libraries/decks/videos/record-a-sound.jpg
index afc27c0ce8f..355ff325b4a 100644
Binary files a/src/lib/libraries/decks/videos/record-a-sound.jpg and b/src/lib/libraries/decks/videos/record-a-sound.jpg differ
diff --git a/src/lib/libraries/decks/videos/spin.jpg b/src/lib/libraries/decks/videos/spin.jpg
index c9c541fc533..8c7004f3920 100644
Binary files a/src/lib/libraries/decks/videos/spin.jpg and b/src/lib/libraries/decks/videos/spin.jpg differ
diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx
index dfe1416464f..4b5a4db07c6 100644
--- a/src/lib/libraries/extensions/index.jsx
+++ b/src/lib/libraries/extensions/index.jsx
@@ -4,18 +4,18 @@ import {FormattedMessage} from 'react-intl';
import musicImage from './music.png';
import penImage from './pen.png';
import videoImage from './video-sensing.png';
-import speechImage from './speech.png';
+import translateImage from './translate.png';
import microbitImage from './microbit.png';
-import wedoImage from './wedo.png';
import ev3Image from './ev3.png';
-import boostImage from './boost.png';
-import translateImage from './translate.png';
-
-import ev3DeviceImage from './device-connection/ev3/ev3-hub-illustration.svg';
-import ev3MenuImage from './device-connection/ev3/ev3-small.svg';
+import wedoImage from './wedo.png';
-import microbitDeviceImage from './device-connection/microbit/microbit-illustration.svg';
-import microbitMenuImage from './device-connection/microbit/microbit-small.svg';
+import microbitPeripheralImage from './peripheral-connection/microbit/microbit-illustration.svg';
+import microbitMenuImage from './peripheral-connection/microbit/microbit-small.svg';
+import ev3PeripheralImage from './peripheral-connection/ev3/ev3-hub-illustration.svg';
+import ev3MenuImage from './peripheral-connection/ev3/ev3-small.svg';
+import wedoPeripheralImage from './peripheral-connection/wedo/wedo-illustration.svg';
+import wedoMenuImage from './peripheral-connection/wedo/wedo-small.svg';
+import wedoButtonImage from './peripheral-connection/wedo/wedo-button-illustration.svg';
export default [
{
@@ -59,37 +59,18 @@ export default [
{
name: (
- ),
- extensionId: 'translate',
- iconURL: translateImage,
- description: (
-
- ),
- featured: true
- },
- {
- name: (
-
),
extensionId: 'videoSensing',
iconURL: videoImage,
description: (
),
featured: true
@@ -97,22 +78,21 @@ export default [
{
name: (
),
- extensionId: 'speech',
- iconURL: speechImage,
+ extensionId: 'translate',
+ iconURL: translateImage,
description: (
),
- featured: true,
- disabled: true
+ featured: true
},
{
name: 'micro:bit',
@@ -126,24 +106,19 @@ export default [
/>
),
featured: true,
- disabled: true,
- launchDeviceConnectionFlow: true,
- deviceImage: microbitDeviceImage,
- smallDeviceImage: microbitMenuImage
- },
- {
- name: 'LEGO WeDo 2.0',
- extensionId: 'wedo2',
- iconURL: wedoImage,
- description: (
+ disabled: false,
+ launchPeripheralConnectionFlow: true,
+ useAutoScan: false,
+ peripheralImage: microbitPeripheralImage,
+ smallPeripheralImage: microbitMenuImage,
+ connectingMessage: (
),
- featured: true,
- disabled: true
+ helpLink: 'https://scratch.mit.edu/microbit'
},
{
name: 'LEGO MINDSTORMS EV3',
@@ -157,23 +132,46 @@ export default [
/>
),
featured: true,
- disabled: true,
- launchDeviceConnectionFlow: true,
- deviceImage: ev3DeviceImage,
- smallDeviceImage: ev3MenuImage
+ disabled: false,
+ launchPeripheralConnectionFlow: true,
+ useAutoScan: false,
+ peripheralImage: ev3PeripheralImage,
+ smallPeripheralImage: ev3MenuImage,
+ connectingMessage: (
+
+ ),
+ helpLink: 'https://scratch.mit.edu/ev3'
},
{
- name: 'LEGO Boost',
- extensionId: 'boost',
- iconURL: boostImage,
+ name: 'LEGO WeDo 2.0',
+ extensionId: 'wedo2',
+ iconURL: wedoImage,
description: (
),
featured: true,
- disabled: true
+ disabled: false,
+ launchPeripheralConnectionFlow: true,
+ useAutoScan: true,
+ peripheralImage: wedoPeripheralImage,
+ smallPeripheralImage: wedoMenuImage,
+ peripheralButtonImage: wedoButtonImage,
+ connectingMessage: (
+
+ ),
+ helpLink: 'https://scratch.mit.edu/wedo'
+
}
];
diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/ev3/ev3-hub-illustration.svg
similarity index 100%
rename from src/lib/libraries/extensions/device-connection/ev3/ev3-hub-illustration.svg
rename to src/lib/libraries/extensions/peripheral-connection/ev3/ev3-hub-illustration.svg
diff --git a/src/lib/libraries/extensions/device-connection/ev3/ev3-small.svg b/src/lib/libraries/extensions/peripheral-connection/ev3/ev3-small.svg
similarity index 100%
rename from src/lib/libraries/extensions/device-connection/ev3/ev3-small.svg
rename to src/lib/libraries/extensions/peripheral-connection/ev3/ev3-small.svg
diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/microbit/microbit-illustration.svg
similarity index 100%
rename from src/lib/libraries/extensions/device-connection/microbit/microbit-illustration.svg
rename to src/lib/libraries/extensions/peripheral-connection/microbit/microbit-illustration.svg
diff --git a/src/lib/libraries/extensions/device-connection/microbit/microbit-small.svg b/src/lib/libraries/extensions/peripheral-connection/microbit/microbit-small.svg
similarity index 100%
rename from src/lib/libraries/extensions/device-connection/microbit/microbit-small.svg
rename to src/lib/libraries/extensions/peripheral-connection/microbit/microbit-small.svg
diff --git a/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-button-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-button-illustration.svg
new file mode 100644
index 00000000000..d874b966961
--- /dev/null
+++ b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-button-illustration.svg
@@ -0,0 +1,74 @@
+
+
+
+ wedo-button-illustration
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-illustration.svg b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-illustration.svg
new file mode 100644
index 00000000000..e82660009ea
--- /dev/null
+++ b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-illustration.svg
@@ -0,0 +1,64 @@
+
+
+
+ wedo-illustration
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-small.svg b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-small.svg
new file mode 100644
index 00000000000..70db707eb8a
--- /dev/null
+++ b/src/lib/libraries/extensions/peripheral-connection/wedo/wedo-small.svg
@@ -0,0 +1,58 @@
+
+
+
+ wedo2-block-icon
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/libraries/extensions/translate.png b/src/lib/libraries/extensions/translate.png
index 95961ae52fe..8f63ade3758 100644
Binary files a/src/lib/libraries/extensions/translate.png and b/src/lib/libraries/extensions/translate.png differ
diff --git a/src/lib/libraries/sound-tags.js b/src/lib/libraries/sound-tags.js
index 61a37002e62..4cba1d57b34 100644
--- a/src/lib/libraries/sound-tags.js
+++ b/src/lib/libraries/sound-tags.js
@@ -1,11 +1,12 @@
+import messages from './tag-messages.js';
export default [
- {title: 'Animals'},
- {title: 'Effects'},
- {title: 'Loops'},
- {title: 'Notes'},
- {title: 'Percussion'},
- {title: 'Space'},
- {title: 'Sports'},
- {title: 'Voice'},
- {title: 'Wacky'}
+ {tag: 'animals', intlLabel: messages.animals},
+ {tag: 'effects', intlLabel: messages.effects},
+ {tag: 'loops', intlLabel: messages.loops},
+ {tag: 'notes', intlLabel: messages.notes},
+ {tag: 'percussion', intlLabel: messages.percussion},
+ {tag: 'space', intlLabel: messages.space},
+ {tag: 'sports', intlLabel: messages.sports},
+ {tag: 'voice', intlLabel: messages.voice},
+ {tag: 'wacky', intlLabel: messages.wacky}
];
diff --git a/src/lib/libraries/sounds.json b/src/lib/libraries/sounds.json
index f62f90b0291..15934e35743 100644
--- a/src/lib/libraries/sounds.json
+++ b/src/lib/libraries/sounds.json
@@ -1118,6 +1118,14 @@
"electronic"
]
},
+ {
+ "name": "Dance Celebrate2",
+ "md5": "0edb8fb88af19e6e17d0f8cf64c1d136.wav",
+ "sampleCount": 176401,
+ "rate": 22050,
+ "format": "adpcm",
+ "tags": []
+ },
{
"name": "Dance Chill Out",
"md5": "b235da45581b1f212c9e9cce70d2a2dc.wav",
diff --git a/src/lib/libraries/sprite-tags.js b/src/lib/libraries/sprite-tags.js
index f1fbe3d5647..8a12e1e41e5 100644
--- a/src/lib/libraries/sprite-tags.js
+++ b/src/lib/libraries/sprite-tags.js
@@ -1,10 +1,11 @@
+import messages from './tag-messages.js';
export default [
- {title: 'Animals'},
- {title: 'People'},
- {title: 'Fantasy'},
- {title: 'Dance'},
- {title: 'Music'},
- {title: 'Sports'},
- {title: 'Food'},
- {title: 'Fashion'}
+ {tag: 'animals', intlLabel: messages.animals},
+ {tag: 'people', intlLabel: messages.people},
+ {tag: 'fantasy', intlLabel: messages.fantasy},
+ {tag: 'dance', intlLabel: messages.dance},
+ {tag: 'music', intlLabel: messages.music},
+ {tag: 'sports', intlLabel: messages.sports},
+ {tag: 'food', intlLabel: messages.food},
+ {tag: 'fashion', intlLabel: messages.fashion}
];
diff --git a/src/lib/libraries/sprites.json b/src/lib/libraries/sprites.json
index 904d2ae9387..3b9596b7ab5 100644
--- a/src/lib/libraries/sprites.json
+++ b/src/lib/libraries/sprites.json
@@ -116,9 +116,84 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Andie",
+ "md5": "936abdaaac3a3f7d27e5f30b3da00e5f.svg",
+ "type": "sprite",
+ "tags": [
+ "sports",
+ "basketball",
+ "people",
+ "wheelchair",
+ "handicap",
+ "handicapable",
+ "alex eben meyer"
+ ],
+ "info": [
+ 0,
+ 4,
+ 1
+ ],
+ "json": {
+ "objName": "Andie",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "andie-a",
+ "baseLayerID": -1,
+ "baseLayerMD5": "936abdaaac3a3f7d27e5f30b3da00e5f.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 86,
+ "rotationCenterY": 77
+ },
+ {
+ "costumeName": "andie-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "cc2953492b40e231fdee3d8c9994a646.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 46,
+ "rotationCenterY": 77
+ },
+ {
+ "costumeName": "andie-c",
+ "baseLayerID": -1,
+ "baseLayerMD5": "76e26ccb79a94d3e89f67e64b4cb9731.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 52,
+ "rotationCenterY": 36
+ },
+ {
+ "costumeName": "andie-d",
+ "baseLayerID": -1,
+ "baseLayerMD5": "80fa011244f3e3be56b96ebcb9e40d59.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 86,
+ "rotationCenterY": 43
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": -129,
+ "scratchY": -73,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Anina Dance",
- "md5": "9374e067ca4b66427fc64a0822f6e468.png",
+ "md5": "b66762519d9542c0ab4fe754b47fe593.png",
"type": "sprite",
"tags": [
"people",
@@ -144,8 +219,8 @@
"costumes": [
{
"costumeName": "anina stance",
- "baseLayerID": -1,
- "baseLayerMD5": "9374e067ca4b66427fc64a0822f6e468.png",
+ "baseLayerID": 0,
+ "baseLayerMD5": "b66762519d9542c0ab4fe754b47fe593.png",
"bitmapResolution": 2,
"rotationCenterX": 76,
"rotationCenterY": 252
@@ -176,24 +251,24 @@
},
{
"costumeName": "anina top freeze",
- "baseLayerID": -1,
- "baseLayerMD5": "a1c9b65dd07747964cce642f9099921b.png",
+ "baseLayerID": 1,
+ "baseLayerMD5": "b7693bd6250d4411ee622b67f8025924.png",
"bitmapResolution": 2,
"rotationCenterX": 110,
"rotationCenterY": 268
},
{
"costumeName": "anina R cross",
- "baseLayerID": -1,
- "baseLayerMD5": "f0d4dce1006cf447a0a4b53e0a8d0519.png",
+ "baseLayerID": 2,
+ "baseLayerMD5": "3948aad16f8169c013c956dd152a09a6.png",
"bitmapResolution": 2,
"rotationCenterX": 126,
"rotationCenterY": 268
},
{
"costumeName": "anina pop front",
- "baseLayerID": -1,
- "baseLayerMD5": "832a3ae650a1f3bede1e0c9f1779e535.png",
+ "baseLayerID": 3,
+ "baseLayerMD5": "4931a363e3e4efa20230f6ff2991c6b4.png",
"bitmapResolution": 2,
"rotationCenterX": 68,
"rotationCenterY": 270
@@ -241,7 +316,7 @@
{
"costumeName": "anina pop R arm",
"baseLayerID": -1,
- "baseLayerMD5": "eac2b5f7db5e81d00c43486179b8dcf8.png",
+ "baseLayerMD5": "ca27e001a263ee6b5852508f39d021db.png",
"bitmapResolution": 2,
"rotationCenterX": 88,
"rotationCenterY": 272
@@ -810,8 +885,8 @@
}
],
"currentCostumeIndex": 0,
- "scratchX": 77,
- "scratchY": -115,
+ "scratchX": 202,
+ "scratchY": -108,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -822,7 +897,7 @@
},
{
"name": "Bat",
- "md5": "a68f7a0b2af7514c729bb083d04dbbde.svg",
+ "md5": "3b6274510488d5b26447c1c266475801.svg",
"type": "sprite",
"tags": [
"fantasy",
@@ -853,39 +928,39 @@
{
"costumeName": "bat-a",
"baseLayerID": -1,
- "baseLayerMD5": "a68f7a0b2af7514c729bb083d04dbbde.svg",
+ "baseLayerMD5": "3b6274510488d5b26447c1c266475801.svg",
"bitmapResolution": 1,
- "rotationCenterX": 51,
- "rotationCenterY": 47
+ "rotationCenterX": 65,
+ "rotationCenterY": 61
},
{
"costumeName": "bat-b",
"baseLayerID": -1,
- "baseLayerMD5": "4ea5bb61370e189d2eba763312680201.svg",
+ "baseLayerMD5": "8b20987d450379a445cbfc41ff2a3874.svg",
"bitmapResolution": 1,
- "rotationCenterX": 51,
- "rotationCenterY": 47
+ "rotationCenterX": 65,
+ "rotationCenterY": 61
},
{
"costumeName": "bat-c",
"baseLayerID": -1,
- "baseLayerMD5": "9ed4deead22db511202f38abadf508c4.svg",
+ "baseLayerMD5": "c6d6a6892638e489902fbba803050de1.svg",
"bitmapResolution": 1,
- "rotationCenterX": 51,
- "rotationCenterY": 47
+ "rotationCenterX": 65,
+ "rotationCenterY": 61
},
{
"costumeName": "bat-d",
"baseLayerID": -1,
- "baseLayerMD5": "293a7b03badd2f3b49e50acfb9c242ec.svg",
+ "baseLayerMD5": "e3da64838b2c74b6dd9a080b030e327e.svg",
"bitmapResolution": 1,
- "rotationCenterX": 51,
- "rotationCenterY": 47
+ "rotationCenterX": 65,
+ "rotationCenterY": 61
}
],
"currentCostumeIndex": 0,
- "scratchX": -39,
- "scratchY": 121,
+ "scratchX": -48,
+ "scratchY": 109,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -1448,6 +1523,51 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Bowtie1",
+ "md5": "aebd82272a5f4a1dd3c23d6e98537017.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 0,
+ 1,
+ 1
+ ],
+ "json": {
+ "objName": "Bowtie1",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "bowtie-01",
+ "baseLayerID": -1,
+ "baseLayerMD5": "aebd82272a5f4a1dd3c23d6e98537017.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 13,
+ "rotationCenterY": 7
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": -65,
+ "scratchY": 58,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Bread",
"md5": "68366160ce0ac1221cdde4455eca9cba.svg",
@@ -2090,7 +2210,7 @@
"costumes": [
{
"costumeName": "cake-a",
- "baseLayerID": 0,
+ "baseLayerID": -1,
"baseLayerMD5": "e1e8e8a765b8778d6181035c5c66984d.svg",
"bitmapResolution": 1,
"rotationCenterX": 64,
@@ -2098,7 +2218,7 @@
},
{
"costumeName": "cake-b",
- "baseLayerID": 1,
+ "baseLayerID": -1,
"baseLayerMD5": "460268a804e7682c9fabf37e4b70071c.svg",
"bitmapResolution": 1,
"rotationCenterX": 64,
@@ -2343,7 +2463,7 @@
],
"costumes": [
{
- "costumeName": "cat1",
+ "costumeName": "cat-a",
"baseLayerID": -1,
"baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg",
"bitmapResolution": 1,
@@ -2351,7 +2471,7 @@
"rotationCenterY": 55
},
{
- "costumeName": "cat2",
+ "costumeName": "cat-b",
"baseLayerID": -1,
"baseLayerMD5": "3696356a03a8d938318876a593572843.svg",
"bitmapResolution": 1,
@@ -2371,53 +2491,46 @@
}
},
{
- "name": "Cat1 Flying",
- "md5": "1e81725d2d2c7de4a2dd4a145198a43c.svg",
+ "name": "Cat 2",
+ "md5": "01ae57fd339529445cb890978ef8a054.svg",
"type": "sprite",
"tags": [
- "animals",
"cat",
"kitty",
- "kitten"
+ "kitten",
+ "animal",
+ "mammal"
],
"info": [
0,
- 2,
+ 1,
1
],
"json": {
- "objName": "Cat1 Flying",
+ "objName": "Cat 2",
"sounds": [
{
- "soundName": "pop",
+ "soundName": "meow2",
"soundID": -1,
- "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
- "sampleCount": 258,
+ "md5": "cf51a0c4088942d95bcc20af13202710.wav",
+ "sampleCount": 6512,
"rate": 11025,
"format": ""
}
],
"costumes": [
{
- "costumeName": "cat1 flying-a",
- "baseLayerID": -1,
- "baseLayerMD5": "1e81725d2d2c7de4a2dd4a145198a43c.svg",
- "bitmapResolution": 1,
- "rotationCenterX": 67,
- "rotationCenterY": 48
- },
- {
- "costumeName": "cat1 flying-b",
+ "costumeName": "cat 2",
"baseLayerID": -1,
- "baseLayerMD5": "0d192725870ef0eda50d91cab0e3c9c5.svg",
+ "baseLayerMD5": "01ae57fd339529445cb890978ef8a054.svg",
"bitmapResolution": 1,
- "rotationCenterX": 42,
- "rotationCenterY": 44
+ "rotationCenterX": 87,
+ "rotationCenterY": 39
}
],
"currentCostumeIndex": 0,
- "scratchX": -53,
- "scratchY": -14,
+ "scratchX": -71,
+ "scratchY": 1,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -2427,46 +2540,53 @@
}
},
{
- "name": "Cat2",
- "md5": "01ae57fd339529445cb890978ef8a054.svg",
+ "name": "Cat Flying",
+ "md5": "1e81725d2d2c7de4a2dd4a145198a43c.svg",
"type": "sprite",
"tags": [
+ "animals",
"cat",
"kitty",
- "kitten",
- "animal",
- "mammal"
+ "kitten"
],
"info": [
0,
- 1,
+ 2,
1
],
"json": {
- "objName": "Cat2",
+ "objName": "Cat Flying",
"sounds": [
{
- "soundName": "meow2",
+ "soundName": "pop",
"soundID": -1,
- "md5": "cf51a0c4088942d95bcc20af13202710.wav",
- "sampleCount": 6512,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
"rate": 11025,
"format": ""
}
],
"costumes": [
{
- "costumeName": "cat2",
+ "costumeName": "cat flying-a",
"baseLayerID": -1,
- "baseLayerMD5": "01ae57fd339529445cb890978ef8a054.svg",
+ "baseLayerMD5": "1e81725d2d2c7de4a2dd4a145198a43c.svg",
"bitmapResolution": 1,
- "rotationCenterX": 87,
- "rotationCenterY": 39
+ "rotationCenterX": 67,
+ "rotationCenterY": 48
+ },
+ {
+ "costumeName": "cat flying-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "0d192725870ef0eda50d91cab0e3c9c5.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 42,
+ "rotationCenterY": 44
}
],
"currentCostumeIndex": 0,
- "scratchX": -71,
- "scratchY": 1,
+ "scratchX": -53,
+ "scratchY": -14,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -2621,7 +2741,7 @@
},
{
"name": "Champ99",
- "md5": "9e4e87818854cfc281d8dcc3e21ee672.png",
+ "md5": "1cb9d1a98cebc8a1d83af7633fdb9cd0.png",
"type": "sprite",
"tags": [
"people",
@@ -2647,8 +2767,8 @@
"costumes": [
{
"costumeName": "champ99-a",
- "baseLayerID": -1,
- "baseLayerMD5": "9e4e87818854cfc281d8dcc3e21ee672.png",
+ "baseLayerID": 6,
+ "baseLayerMD5": "1cb9d1a98cebc8a1d83af7633fdb9cd0.png",
"bitmapResolution": 2,
"rotationCenterX": 248,
"rotationCenterY": 306
@@ -3144,7 +3264,7 @@
},
{
"name": "D-Money Dance",
- "md5": "b30a60d31aebead577b73affd22bf30f.svg",
+ "md5": "dafbdfe454c5ec7029b5c1e07fcabc90.png",
"type": "sprite",
"tags": [
"people",
@@ -3170,16 +3290,16 @@
"costumes": [
{
"costumeName": "dm stance",
- "baseLayerID": -1,
- "baseLayerMD5": "b30a60d31aebead577b73affd22bf30f.svg",
- "bitmapResolution": 1,
- "rotationCenterX": 55,
- "rotationCenterY": 119
+ "baseLayerID": 7,
+ "baseLayerMD5": "dafbdfe454c5ec7029b5c1e07fcabc90.png",
+ "bitmapResolution": 2,
+ "rotationCenterX": 106,
+ "rotationCenterY": 238
},
{
"costumeName": "dm top stand",
"baseLayerID": -1,
- "baseLayerMD5": "051c36758b9fd1136b511efc5dd6234a.png",
+ "baseLayerMD5": "a22da98e5e63de7b2883355afd0184f0.png",
"bitmapResolution": 2,
"rotationCenterX": 82,
"rotationCenterY": 244
@@ -3187,7 +3307,7 @@
{
"costumeName": "dm top R leg",
"baseLayerID": -1,
- "baseLayerMD5": "6cea0c29c460e571226cc4cff5b97c28.png",
+ "baseLayerMD5": "12db59633a1709a2c39534d35263791f.png",
"bitmapResolution": 2,
"rotationCenterX": 218,
"rotationCenterY": 232
@@ -4177,8 +4297,8 @@
}
],
"currentCostumeIndex": 0,
- "scratchX": -108,
- "scratchY": -61,
+ "scratchX": -24,
+ "scratchY": -57,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -4377,6 +4497,67 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Dress",
+ "md5": "308b6ad8e6cfe5c2b2c1e3a1eb9cc404.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 0,
+ 3,
+ 1
+ ],
+ "json": {
+ "objName": "Dress",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "dress-01",
+ "baseLayerID": -1,
+ "baseLayerMD5": "308b6ad8e6cfe5c2b2c1e3a1eb9cc404.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 58,
+ "rotationCenterY": 83
+ },
+ {
+ "costumeName": "dress-02",
+ "baseLayerID": -1,
+ "baseLayerMD5": "1f7a725ba285dc97cb142d4b812db624.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 49,
+ "rotationCenterY": 83
+ },
+ {
+ "costumeName": "dress-03",
+ "baseLayerID": -1,
+ "baseLayerMD5": "fa7e0b37a324c212d0f1ce0f701a1ae0.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 57,
+ "rotationCenterY": 83
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": -65,
+ "scratchY": -50,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Drum",
"md5": "dd66742bc2a3cfe5a6f9f540afd2e15c.svg",
@@ -4850,7 +5031,7 @@
"costumes": [
{
"costumeName": "egg-a",
- "baseLayerID": 5,
+ "baseLayerID": -1,
"baseLayerMD5": "bc723738dfe626c5c3bb90970d985961.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -4858,7 +5039,7 @@
},
{
"costumeName": "egg-b",
- "baseLayerID": 6,
+ "baseLayerID": -1,
"baseLayerMD5": "83016b7ff817f99be4a454600b4a78fc.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -4866,7 +5047,7 @@
},
{
"costumeName": "egg-c",
- "baseLayerID": 7,
+ "baseLayerID": -1,
"baseLayerMD5": "d9a44d151fbd909bdbbcf7877055af6d.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -4874,7 +5055,7 @@
},
{
"costumeName": "egg-d",
- "baseLayerID": 8,
+ "baseLayerID": -1,
"baseLayerMD5": "c91c7f72b8523b0910a84bce7d99c37b.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -4882,7 +5063,7 @@
},
{
"costumeName": "egg-e",
- "baseLayerID": 9,
+ "baseLayerID": -1,
"baseLayerMD5": "65c15516e62596e1f72e874359fc7254.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -5457,8 +5638,8 @@
}
],
"currentCostumeIndex": 0,
- "scratchX": -110,
- "scratchY": -24,
+ "scratchX": -115,
+ "scratchY": -46,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -5957,12 +6138,12 @@
"objName": "Glass Water",
"sounds": [
{
- "soundName": "pop",
+ "soundName": "water drop",
"soundID": -1,
- "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
- "sampleCount": 258,
- "rate": 11025,
- "format": ""
+ "md5": "aa488de9e2c871e9d4faecd246ed737a.wav",
+ "sampleCount": 8136,
+ "rate": 22050,
+ "format": "adpcm"
}
],
"costumes": [
@@ -5996,15 +6177,15 @@
},
{
"name": "Glasses",
- "md5": "5fcf716b53f223bc86b10ab0eca3e162.svg",
+ "md5": "f42180039c37c3b221e1e426b92e5092.svg",
"type": "sprite",
"tags": [
- "things",
- "fashion"
+ "fashion",
+ "glasses"
],
"info": [
0,
- 1,
+ 4,
1
],
"json": {
@@ -6021,17 +6202,41 @@
],
"costumes": [
{
- "costumeName": "glasses",
+ "costumeName": "glasses-a",
"baseLayerID": -1,
- "baseLayerMD5": "5fcf716b53f223bc86b10ab0eca3e162.svg",
+ "baseLayerMD5": "f42180039c37c3b221e1e426b92e5092.svg",
"bitmapResolution": 1,
- "rotationCenterX": 16,
- "rotationCenterY": 9
+ "rotationCenterX": 33,
+ "rotationCenterY": 13
+ },
+ {
+ "costumeName": "glasses-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "58b728ee8c1f739e2de13800dee562de.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 35,
+ "rotationCenterY": 14
+ },
+ {
+ "costumeName": "glasses-c",
+ "baseLayerID": -1,
+ "baseLayerMD5": "d8bf373e1fcbb46f6eb8d225ba01613e.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 34,
+ "rotationCenterY": 12
+ },
+ {
+ "costumeName": "glasses-e",
+ "baseLayerID": -1,
+ "baseLayerMD5": "6b706718a8c350522c3f29fb82b40327.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 39,
+ "rotationCenterY": 53
}
],
"currentCostumeIndex": 0,
- "scratchX": 65,
- "scratchY": 6,
+ "scratchX": 37,
+ "scratchY": 102,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -6873,6 +7078,68 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Harper",
+ "md5": "919eb332bf73df227ec5f214be952a21.svg",
+ "type": "sprite",
+ "tags": [
+ "people",
+ "fashion"
+ ],
+ "info": [
+ 0,
+ 3,
+ 1
+ ],
+ "json": {
+ "objName": "Harper",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "harper-a",
+ "baseLayerID": -1,
+ "baseLayerMD5": "919eb332bf73df227ec5f214be952a21.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 54,
+ "rotationCenterY": 135
+ },
+ {
+ "costumeName": "harper-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "0d5979db40b0a4dcaa601fb7679ef71c.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 54,
+ "rotationCenterY": 143
+ },
+ {
+ "costumeName": "harper-c",
+ "baseLayerID": -1,
+ "baseLayerMD5": "9c35763ec87604ff1b4848ff18e0b040.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 56,
+ "rotationCenterY": 138
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": 38,
+ "scratchY": 18,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Hat",
"md5": "b3beb1f52d371428d70b65a0c4c5c001.svg",
@@ -7147,6 +7414,76 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Hat1 ",
+ "md5": "874462b3e079308d775629c235940d8f.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion",
+ "hat"
+ ],
+ "info": [
+ 0,
+ 4,
+ 1
+ ],
+ "json": {
+ "objName": "Hat1 ",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "hat-a",
+ "baseLayerID": -1,
+ "baseLayerMD5": "874462b3e079308d775629c235940d8f.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 39,
+ "rotationCenterY": 22
+ },
+ {
+ "costumeName": "hat-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "a8ef874bd7542fec62785eac80019417.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 45,
+ "rotationCenterY": 30
+ },
+ {
+ "costumeName": "hat-c",
+ "baseLayerID": -1,
+ "baseLayerMD5": "52540f9dba4537f79f1ad3067d44e3f6.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 37,
+ "rotationCenterY": 30
+ },
+ {
+ "costumeName": "hat-d",
+ "baseLayerID": -1,
+ "baseLayerMD5": "b40accecd4bd57fed8bdb96ee15e5c05.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 40,
+ "rotationCenterY": 27
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": -70,
+ "scratchY": 126,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Hatchling",
"md5": "fb9de507b6d95e96cb0df68e401dfd8f.svg",
@@ -7239,7 +7576,7 @@
"costumes": [
{
"costumeName": "headband",
- "baseLayerID": 0,
+ "baseLayerID": -1,
"baseLayerMD5": "237f77cb3e41c70f714c6d2aa4d38ea1.svg",
"bitmapResolution": 1,
"rotationCenterX": 53,
@@ -7458,7 +7795,7 @@
"costumes": [
{
"costumeName": "hedgehog-a",
- "baseLayerID": 0,
+ "baseLayerID": -1,
"baseLayerMD5": "32416e6b2ef8e45fb5fd10778c1b9a9f.svg",
"bitmapResolution": 1,
"rotationCenterX": 71,
@@ -7466,7 +7803,7 @@
},
{
"costumeName": "hedgehog-b",
- "baseLayerID": 1,
+ "baseLayerID": -1,
"baseLayerMD5": "4d3ccc06660e07b55bd38246e1f82f7f.svg",
"bitmapResolution": 1,
"rotationCenterX": 71,
@@ -7474,7 +7811,7 @@
},
{
"costumeName": "hedgehog-c",
- "baseLayerID": 2,
+ "baseLayerID": -1,
"baseLayerMD5": "2446f79c0f553594cfbcdbe6b1e459a5.svg",
"bitmapResolution": 1,
"rotationCenterX": 71,
@@ -7482,7 +7819,7 @@
},
{
"costumeName": "hedgehog-d",
- "baseLayerID": 3,
+ "baseLayerID": -1,
"baseLayerMD5": "bdb7c8e86125092da0c4848d1ffd901c.svg",
"bitmapResolution": 1,
"rotationCenterX": 71,
@@ -7490,7 +7827,7 @@
},
{
"costumeName": "hedgehog-e",
- "baseLayerID": 4,
+ "baseLayerID": -1,
"baseLayerMD5": "78a0e3789f6d778e20f9bf3d308a0b19.svg",
"bitmapResolution": 1,
"rotationCenterX": 61,
@@ -7846,7 +8183,7 @@
}
},
{
- "name": "Jamie",
+ "name": "Jamal",
"md5": "90f9166fe6500d0c0caad8b1964d6b74.svg",
"type": "sprite",
"tags": [
@@ -7861,7 +8198,7 @@
1
],
"json": {
- "objName": "Jamie",
+ "objName": "Jamal",
"sounds": [
{
"soundName": "pop",
@@ -7874,7 +8211,7 @@
],
"costumes": [
{
- "costumeName": "jamie-a",
+ "costumeName": "jamal-a",
"baseLayerID": -1,
"baseLayerMD5": "90f9166fe6500d0c0caad8b1964d6b74.svg",
"bitmapResolution": 1,
@@ -7882,7 +8219,7 @@
"rotationCenterY": 105
},
{
- "costumeName": "jamie-b",
+ "costumeName": "jamal-b",
"baseLayerID": -1,
"baseLayerMD5": "c3d96ef7e99440c2fa76effce1235d3f.svg",
"bitmapResolution": 1,
@@ -7890,7 +8227,7 @@
"rotationCenterY": 105
},
{
- "costumeName": "jamie-c",
+ "costumeName": "jamal-c",
"baseLayerID": -1,
"baseLayerMD5": "1fb8b9ca79f2c0a327913bd647b53fe5.svg",
"bitmapResolution": 1,
@@ -7898,7 +8235,7 @@
"rotationCenterY": 105
},
{
- "costumeName": "jamie-d",
+ "costumeName": "jamal-d",
"baseLayerID": -1,
"baseLayerMD5": "4adb87e6123161fcaf02f7ac022a5757.svg",
"bitmapResolution": 1,
@@ -8174,7 +8511,7 @@
},
{
"name": "Jouvi Dance",
- "md5": "e10d129d9be2147bc7d284e281d88e9a.png",
+ "md5": "6f68790ee3eb9bdccf8749305186b0dd.png",
"type": "sprite",
"tags": [
"people",
@@ -8200,8 +8537,8 @@
"costumes": [
{
"costumeName": "jo stance",
- "baseLayerID": -1,
- "baseLayerMD5": "e10d129d9be2147bc7d284e281d88e9a.png",
+ "baseLayerID": 8,
+ "baseLayerMD5": "6f68790ee3eb9bdccf8749305186b0dd.png",
"bitmapResolution": 2,
"rotationCenterX": 94,
"rotationCenterY": 240
@@ -8224,7 +8561,7 @@
},
{
"costumeName": "jo top L leg",
- "baseLayerID": 0,
+ "baseLayerID": -1,
"baseLayerMD5": "a12f40b18067bb31746f9cf461de88aa.png",
"bitmapResolution": 2,
"rotationCenterX": 208,
@@ -8232,7 +8569,7 @@
},
{
"costumeName": "jo top R cross",
- "baseLayerID": 1,
+ "baseLayerID": -1,
"baseLayerMD5": "c2d5519e8a0f2214ff757117038c28dc.png",
"bitmapResolution": 2,
"rotationCenterX": 144,
@@ -8240,7 +8577,7 @@
},
{
"costumeName": "jo top L cross",
- "baseLayerID": 2,
+ "baseLayerID": -1,
"baseLayerMD5": "2e2a6534d33883fdd2f8471a1adbebb7.png",
"bitmapResolution": 2,
"rotationCenterX": 84,
@@ -8248,7 +8585,7 @@
},
{
"costumeName": "jo pop front",
- "baseLayerID": 3,
+ "baseLayerID": -1,
"baseLayerMD5": "3d3ea804243800981acabc7caba10939.png",
"bitmapResolution": 2,
"rotationCenterX": 70,
@@ -8256,7 +8593,7 @@
},
{
"costumeName": "jo pop down",
- "baseLayerID": 4,
+ "baseLayerID": -1,
"baseLayerMD5": "a55fbb529c10f70bcb374aef8a63571b.png",
"bitmapResolution": 2,
"rotationCenterX": 68,
@@ -8264,7 +8601,7 @@
},
{
"costumeName": "jo pop left",
- "baseLayerID": 5,
+ "baseLayerID": -1,
"baseLayerMD5": "ea812b4c2b2405aa2b73158023298f71.png",
"bitmapResolution": 2,
"rotationCenterX": 196,
@@ -8272,7 +8609,7 @@
},
{
"costumeName": "jo pop right",
- "baseLayerID": 6,
+ "baseLayerID": -1,
"baseLayerMD5": "01dd2f553c7262329ebaba2516e3a2b1.png",
"bitmapResolution": 2,
"rotationCenterX": 66,
@@ -8280,7 +8617,7 @@
},
{
"costumeName": "jo pop L arm",
- "baseLayerID": 7,
+ "baseLayerID": -1,
"baseLayerMD5": "a9fbc01a4124d555da12630312e46197.png",
"bitmapResolution": 2,
"rotationCenterX": 108,
@@ -8288,7 +8625,7 @@
},
{
"costumeName": "jo pop stand",
- "baseLayerID": 8,
+ "baseLayerID": -1,
"baseLayerMD5": "75ee2383fd83992b401c8a0730521d94.png",
"bitmapResolution": 2,
"rotationCenterX": 78,
@@ -8297,7 +8634,7 @@
{
"costumeName": "jo pop R arm",
"baseLayerID": 9,
- "baseLayerMD5": "a35a3bbc4e21ce2f5de940adb7017fa6.png",
+ "baseLayerMD5": "aabfedff0d11243386b6b0941e0f72e9.png",
"bitmapResolution": 2,
"rotationCenterX": 108,
"rotationCenterY": 260
@@ -8659,7 +8996,7 @@
},
{
"name": "LB Dance",
- "md5": "9d64910e84c39660b19ca4ca8b45e2be.png",
+ "md5": "71dde8c43985815bffb5a5ed5632af58.png",
"type": "sprite",
"tags": [
"people",
@@ -8685,8 +9022,8 @@
"costumes": [
{
"costumeName": "lb stance",
- "baseLayerID": -1,
- "baseLayerMD5": "9d64910e84c39660b19ca4ca8b45e2be.png",
+ "baseLayerID": 0,
+ "baseLayerMD5": "71dde8c43985815bffb5a5ed5632af58.png",
"bitmapResolution": 2,
"rotationCenterX": 54,
"rotationCenterY": 244
@@ -9552,7 +9889,7 @@
"costumes": [
{
"costumeName": "milk-a",
- "baseLayerID": 0,
+ "baseLayerID": -1,
"baseLayerMD5": "e6a7964bc4ea38c79a5a31d6ddfb5ba9.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -9560,7 +9897,7 @@
},
{
"costumeName": "milk-b",
- "baseLayerID": 1,
+ "baseLayerID": -1,
"baseLayerMD5": "82d4c1855fe0d400433c7344fb2af3b5.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -9568,7 +9905,7 @@
},
{
"costumeName": "milk-c",
- "baseLayerID": 2,
+ "baseLayerID": -1,
"baseLayerMD5": "50afc991b6fdad4b6547ba98ecf8a6af.svg",
"bitmapResolution": 1,
"rotationCenterX": 47,
@@ -9576,7 +9913,7 @@
},
{
"costumeName": "milk-d",
- "baseLayerID": 3,
+ "baseLayerID": -1,
"baseLayerMD5": "8fc7606a176149d225a541a04fa67473.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -9584,7 +9921,7 @@
},
{
"costumeName": "milk-e",
- "baseLayerID": 4,
+ "baseLayerID": -1,
"baseLayerMD5": "f2373d449b1226c44436dced422c2935.svg",
"bitmapResolution": 1,
"rotationCenterX": 68,
@@ -10361,7 +10698,7 @@
"costumes": [
{
"costumeName": "panther-a",
- "baseLayerID": 0,
+ "baseLayerID": -1,
"baseLayerMD5": "04ca2c122cff11b9bc23834d6f79361e.svg",
"bitmapResolution": 1,
"rotationCenterX": 125,
@@ -10369,7 +10706,7 @@
},
{
"costumeName": "panther-b",
- "baseLayerID": 1,
+ "baseLayerID": -1,
"baseLayerMD5": "f8c33765d1105f3bb4cd145fad0f717e.svg",
"bitmapResolution": 1,
"rotationCenterX": 125,
@@ -10377,7 +10714,7 @@
},
{
"costumeName": "panther-c",
- "baseLayerID": 2,
+ "baseLayerID": -1,
"baseLayerMD5": "096bf9cad84def12eef2b5d84736b393.svg",
"bitmapResolution": 1,
"rotationCenterX": 125,
@@ -10395,6 +10732,60 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Pants",
+ "md5": "f7a0584eca14eaa0e29662e5c24fb62d.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion",
+ "pants"
+ ],
+ "info": [
+ 0,
+ 2,
+ 1
+ ],
+ "json": {
+ "objName": "Pants",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "pants-a",
+ "baseLayerID": -1,
+ "baseLayerMD5": "f7a0584eca14eaa0e29662e5c24fb62d.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 34,
+ "rotationCenterY": 66
+ },
+ {
+ "costumeName": "pants-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "22ea0cf68b885817a5b6a01194c6f5d3.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 35,
+ "rotationCenterY": 66
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": 148,
+ "scratchY": -49,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Parrot",
"md5": "098570b8e1aa85b32f9b4eb07bea3af2.svg",
@@ -11230,6 +11621,96 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Pumpkin Costume",
+ "md5": "6f7a5a88e6201082913a099438d626ef.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 0,
+ 1,
+ 1
+ ],
+ "json": {
+ "objName": "Pumpkin Costume",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "pumpkin-costume",
+ "baseLayerID": -1,
+ "baseLayerMD5": "6f7a5a88e6201082913a099438d626ef.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 39,
+ "rotationCenterY": 63
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": -169,
+ "scratchY": 13,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
+ {
+ "name": "Pumpkin Hat",
+ "md5": "22bdeaf0d8421ffb70775ed77070bee7.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion"
+ ],
+ "info": [
+ 0,
+ 1,
+ 1
+ ],
+ "json": {
+ "objName": "Pumpkin Hat",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "pumpkin-hat",
+ "baseLayerID": -1,
+ "baseLayerMD5": "22bdeaf0d8421ffb70775ed77070bee7.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 28,
+ "rotationCenterY": 24
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": -172,
+ "scratchY": 126,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Puppy",
"md5": "2768d9e44a0aab055856d301bbc2b04e.png",
@@ -11333,7 +11814,7 @@
"costumes": [
{
"costumeName": "rabbit-a",
- "baseLayerID": 5,
+ "baseLayerID": -1,
"baseLayerMD5": "2f42891c3f3d63c0e591aeabc5946533.svg",
"bitmapResolution": 1,
"rotationCenterX": 84,
@@ -11341,7 +11822,7 @@
},
{
"costumeName": "rabbit-b",
- "baseLayerID": 6,
+ "baseLayerID": -1,
"baseLayerMD5": "80e05ff501040cdc9f52fa6782e06fd2.svg",
"bitmapResolution": 1,
"rotationCenterX": 84,
@@ -11349,7 +11830,7 @@
},
{
"costumeName": "rabbit-c",
- "baseLayerID": 7,
+ "baseLayerID": -1,
"baseLayerMD5": "88ed8b7925baa025b6c7fc628a64b9b1.svg",
"bitmapResolution": 1,
"rotationCenterX": 84,
@@ -11357,7 +11838,7 @@
},
{
"costumeName": "rabbit-d",
- "baseLayerID": 8,
+ "baseLayerID": -1,
"baseLayerMD5": "5f3b8df4d6ab8a72e887f89f554db0be.svg",
"bitmapResolution": 1,
"rotationCenterX": 84,
@@ -11365,7 +11846,7 @@
},
{
"costumeName": "rabbit-e",
- "baseLayerID": 9,
+ "baseLayerID": -1,
"baseLayerMD5": "3003f1135f4aa3b6c361734243621260.svg",
"bitmapResolution": 1,
"rotationCenterX": 84,
@@ -12047,7 +12528,7 @@
"costumes": [
{
"costumeName": "sam",
- "baseLayerID": 0,
+ "baseLayerID": -1,
"baseLayerMD5": "8208e99159b36c957fb9fbc187e51bc7.png",
"bitmapResolution": 2,
"rotationCenterX": 117,
@@ -12390,6 +12871,52 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Shirt",
+ "md5": "659465944053fe6fb6aa1ed0e11be9aa.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion",
+ "shirt"
+ ],
+ "info": [
+ 0,
+ 1,
+ 1
+ ],
+ "json": {
+ "objName": "Shirt",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "shirt-a",
+ "baseLayerID": -1,
+ "baseLayerMD5": "659465944053fe6fb6aa1ed0e11be9aa.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 46,
+ "rotationCenterY": 40
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": 147,
+ "scratchY": 93,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Shirt Blouse",
"md5": "918a507af6bbae9e7f36f0d949900838.svg",
@@ -12594,6 +13121,76 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Shoes",
+ "md5": "4639a1af5bc91f1a6f14e822cd46972f.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion",
+ "shoes"
+ ],
+ "info": [
+ 0,
+ 4,
+ 1
+ ],
+ "json": {
+ "objName": "Shoes",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "shoes-a",
+ "baseLayerID": -1,
+ "baseLayerMD5": "4639a1af5bc91f1a6f14e822cd46972f.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 40,
+ "rotationCenterY": 13
+ },
+ {
+ "costumeName": "shoes-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "6ba2a692c17f47170d611578a5620ae5.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 40,
+ "rotationCenterY": 31
+ },
+ {
+ "costumeName": "shoes-d",
+ "baseLayerID": -1,
+ "baseLayerMD5": "1130f7ca93716bac6afa31af86329e92.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 44,
+ "rotationCenterY": 32
+ },
+ {
+ "costumeName": "shoes-c",
+ "baseLayerID": -1,
+ "baseLayerMD5": "14c6843195bd13824d253cb3fdb9e6de.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 45,
+ "rotationCenterY": 33
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": 49,
+ "scratchY": -140,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Shoes1",
"md5": "ffab4cc284070b50ac317e515f59f7d8.svg",
@@ -12684,6 +13281,70 @@
"spriteInfo": {}
}
},
+ {
+ "name": "Shorts",
+ "md5": "d9b580c913c0d1d1a996dd733af91e68.svg",
+ "type": "sprite",
+ "tags": [
+ "fashion",
+ "pants",
+ "shorts",
+ "clothing"
+ ],
+ "info": [
+ 0,
+ 3,
+ 1
+ ],
+ "json": {
+ "objName": "Shorts",
+ "sounds": [
+ {
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }
+ ],
+ "costumes": [
+ {
+ "costumeName": "shorts-a",
+ "baseLayerID": -1,
+ "baseLayerMD5": "d9b580c913c0d1d1a996dd733af91e68.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 35,
+ "rotationCenterY": 37
+ },
+ {
+ "costumeName": "shorts-b",
+ "baseLayerID": -1,
+ "baseLayerMD5": "1e07ab2763e5e0f5557f97c0e2c89020.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 43,
+ "rotationCenterY": 36
+ },
+ {
+ "costumeName": "shorts-c",
+ "baseLayerID": -1,
+ "baseLayerMD5": "dc9e0a1c2489ec0bf59a937b6c8ae85b.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 35,
+ "rotationCenterY": 29
+ }
+ ],
+ "currentCostumeIndex": 0,
+ "scratchX": -166,
+ "scratchY": -113,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "visible": true,
+ "spriteInfo": {}
+ }
+ },
{
"name": "Singer1",
"md5": "e47ef1af3b925e5ac9e3b3f809d440b3.svg",
@@ -12888,7 +13549,7 @@
"costumes": [
{
"costumeName": "snake-a",
- "baseLayerID": 3,
+ "baseLayerID": -1,
"baseLayerMD5": "4d06e12d90479461303d828f0970f2d4.svg",
"bitmapResolution": 1,
"rotationCenterX": 142,
@@ -12896,7 +13557,7 @@
},
{
"costumeName": "snake-b",
- "baseLayerID": 4,
+ "baseLayerID": -1,
"baseLayerMD5": "a8546a5f9ccaa2bdb678a362c50a17ec.svg",
"bitmapResolution": 1,
"rotationCenterX": 142,
@@ -12904,7 +13565,7 @@
},
{
"costumeName": "snake-c",
- "baseLayerID": 5,
+ "baseLayerID": -1,
"baseLayerMD5": "d993a7d70179777c14ac91a07e711d90.svg",
"bitmapResolution": 1,
"rotationCenterX": 142,
@@ -13404,7 +14065,7 @@
"costumes": [
{
"costumeName": "strawberry-a",
- "baseLayerID": 10,
+ "baseLayerID": -1,
"baseLayerMD5": "734556fb8e14740f2cbc971238b71d47.svg",
"bitmapResolution": 1,
"rotationCenterX": 57,
@@ -13412,7 +14073,7 @@
},
{
"costumeName": "strawberry-b",
- "baseLayerID": 11,
+ "baseLayerID": -1,
"baseLayerMD5": "77dadaa80bc5156f655c2196f57972f7.svg",
"bitmapResolution": 1,
"rotationCenterX": 57,
@@ -13420,7 +14081,7 @@
},
{
"costumeName": "strawberry-c",
- "baseLayerID": 12,
+ "baseLayerID": -1,
"baseLayerMD5": "19f933b3cf3e8e150753f8fb9bb7a779.svg",
"bitmapResolution": 1,
"rotationCenterX": 57,
@@ -13428,7 +14089,7 @@
},
{
"costumeName": "strawberry-d",
- "baseLayerID": 13,
+ "baseLayerID": -1,
"baseLayerMD5": "8e8057da8457e6167de36b7d3d28b4bb.svg",
"bitmapResolution": 1,
"rotationCenterX": 57,
@@ -13436,7 +14097,7 @@
},
{
"costumeName": "strawberry-e",
- "baseLayerID": 14,
+ "baseLayerID": -1,
"baseLayerMD5": "5eb63e64b83f5aa5b75a55329a34efec.svg",
"bitmapResolution": 1,
"rotationCenterX": 57,
@@ -13598,13 +14259,11 @@
},
{
"name": "Taco",
- "md5": "d4a2cea411c6030f017f25184562559d.svg",
+ "md5": "bc78fb90ed373d56c11d5fafa4203ccd.svg",
"type": "sprite",
"tags": [
"food",
- "waffle",
- "yum",
- "delicious"
+ "designerd"
],
"info": [
0,
@@ -13625,25 +14284,25 @@
],
"costumes": [
{
- "costumeName": "taco-a",
+ "costumeName": "Taco",
"baseLayerID": -1,
- "baseLayerMD5": "d4a2cea411c6030f017f25184562559d.svg",
+ "baseLayerMD5": "bc78fb90ed373d56c11d5fafa4203ccd.svg",
"bitmapResolution": 1,
- "rotationCenterX": 19,
- "rotationCenterY": 15
+ "rotationCenterX": 78,
+ "rotationCenterY": 48
},
{
- "costumeName": "taco-b",
+ "costumeName": "Taco-wizard",
"baseLayerID": -1,
- "baseLayerMD5": "cef6f6d7c92fc1f008077319ac12dc5e.svg",
+ "baseLayerMD5": "5e9e65db20d403b590578ed44b1a3792.svg",
"bitmapResolution": 1,
- "rotationCenterX": 56,
- "rotationCenterY": 15
+ "rotationCenterX": 125,
+ "rotationCenterY": 82
}
],
"currentCostumeIndex": 0,
- "scratchX": 7,
- "scratchY": -45,
+ "scratchX": 0,
+ "scratchY": 0,
"scale": 1,
"direction": 90,
"rotationStyle": "normal",
@@ -13680,7 +14339,7 @@
"costumes": [
{
"costumeName": "takeout-a",
- "baseLayerID": 15,
+ "baseLayerID": -1,
"baseLayerMD5": "0cfdefe0df1a032b90c8facd9f5dbe1f.svg",
"bitmapResolution": 1,
"rotationCenterX": 78,
@@ -13688,7 +14347,7 @@
},
{
"costumeName": "takeout-b",
- "baseLayerID": 16,
+ "baseLayerID": -1,
"baseLayerMD5": "48b19c48e32c98a35836ee40e3a7accf.svg",
"bitmapResolution": 1,
"rotationCenterX": 78,
@@ -13696,7 +14355,7 @@
},
{
"costumeName": "takeout-c",
- "baseLayerID": 17,
+ "baseLayerID": -1,
"baseLayerMD5": "22d33d87883f8fb26c642eccc9b339f0.svg",
"bitmapResolution": 1,
"rotationCenterX": 78,
@@ -13704,7 +14363,7 @@
},
{
"costumeName": "takeout-d",
- "baseLayerID": 18,
+ "baseLayerID": -1,
"baseLayerMD5": "262f1a3730d669dc9d43b3853e397361.svg",
"bitmapResolution": 1,
"rotationCenterX": 78,
@@ -13712,7 +14371,7 @@
},
{
"costumeName": "takeout-e",
- "baseLayerID": 19,
+ "baseLayerID": -1,
"baseLayerMD5": "528e9df8c3bd173867be4143f8563e87.svg",
"bitmapResolution": 1,
"rotationCenterX": 78,
@@ -13774,8 +14433,8 @@
},
{
"costumeName": "Ten80 top R step",
- "baseLayerID": -1,
- "baseLayerMD5": "733d05c17699758e2723599de333fdc0.png",
+ "baseLayerID": 4,
+ "baseLayerMD5": "580fba92f23d5592200eb5a9079dc38f.png",
"bitmapResolution": 2,
"rotationCenterX": 200,
"rotationCenterY": 270
@@ -13814,8 +14473,8 @@
},
{
"costumeName": "Ten80 pop down",
- "baseLayerID": -1,
- "baseLayerMD5": "14685e432d683c918328e7fb070b7029.png",
+ "baseLayerID": 5,
+ "baseLayerMD5": "fea7045c09073700b88fae8d4d257cd1.png",
"bitmapResolution": 2,
"rotationCenterX": 74,
"rotationCenterY": 188
@@ -14017,7 +14676,7 @@
"costumes": [
{
"costumeName": "toucan-a",
- "baseLayerID": 6,
+ "baseLayerID": -1,
"baseLayerMD5": "6c8798e606abd728b112aecedb5dc249.svg",
"bitmapResolution": 1,
"rotationCenterX": 80,
@@ -14025,7 +14684,7 @@
},
{
"costumeName": "toucan-b",
- "baseLayerID": 7,
+ "baseLayerID": -1,
"baseLayerMD5": "a3e12be9efa0e7aa83778f6054c9c541.svg",
"bitmapResolution": 1,
"rotationCenterX": 80,
@@ -14033,7 +14692,7 @@
},
{
"costumeName": "toucan-c",
- "baseLayerID": 8,
+ "baseLayerID": -1,
"baseLayerMD5": "56522b58a9959fd6152060346129f7cb.svg",
"bitmapResolution": 1,
"rotationCenterX": 80,
diff --git a/src/lib/libraries/tag-messages.js b/src/lib/libraries/tag-messages.js
new file mode 100644
index 00000000000..60a6d13fc50
--- /dev/null
+++ b/src/lib/libraries/tag-messages.js
@@ -0,0 +1,104 @@
+import {defineMessages} from 'react-intl';
+
+export default defineMessages({
+ all: {
+ defaultMessage: 'All',
+ description: 'Tag for filtering a library for everything',
+ id: 'gui.libraryTags.all'
+ },
+ animals: {
+ defaultMessage: 'Animals',
+ description: 'Tag for filtering a library for animals',
+ id: 'gui.libraryTags.animals'
+ },
+ dance: {
+ defaultMessage: 'Dance',
+ description: 'Tag for filtering a library for dance',
+ id: 'gui.libraryTags.dance'
+ },
+ effects: {
+ defaultMessage: 'Effects',
+ description: 'Tag for filtering a library for effects',
+ id: 'gui.libraryTags.effects'
+ },
+ fantasy: {
+ defaultMessage: 'Fantasy',
+ description: 'Tag for filtering a library for fantasy',
+ id: 'gui.libraryTags.fantasy'
+ },
+ fashion: {
+ defaultMessage: 'Fashion',
+ description: 'Tag for filtering a library for fashion',
+ id: 'gui.libraryTags.fashion'
+ },
+ food: {
+ defaultMessage: 'Food',
+ description: 'Tag for filtering a library for food',
+ id: 'gui.libraryTags.food'
+ },
+ indoors: {
+ defaultMessage: 'Indoors',
+ description: 'Tag for filtering a library for indoors',
+ id: 'gui.libraryTags.indoors'
+ },
+ loops: {
+ defaultMessage: 'Loops',
+ description: 'Tag for filtering a library for loops',
+ id: 'gui.libraryTags.loops'
+ },
+ music: {
+ defaultMessage: 'Music',
+ description: 'Tag for filtering a library for music',
+ id: 'gui.libraryTags.music'
+ },
+ notes: {
+ defaultMessage: 'Notes',
+ description: 'Tag for filtering a library for notes',
+ id: 'gui.libraryTags.notes'
+ },
+ outdoors: {
+ defaultMessage: 'Outdoors',
+ description: 'Tag for filtering a library for outdoors',
+ id: 'gui.libraryTags.outdoors'
+ },
+ patterns: {
+ defaultMessage: 'Patterns',
+ description: 'Tag for filtering a library for patterns',
+ id: 'gui.libraryTags.patterns'
+ },
+ people: {
+ defaultMessage: 'People',
+ description: 'Tag for filtering a library for people',
+ id: 'gui.libraryTags.people'
+ },
+ percussion: {
+ defaultMessage: 'Percussion',
+ description: 'Tag for filtering a library for percussion',
+ id: 'gui.libraryTags.percussion'
+ },
+ space: {
+ defaultMessage: 'Space',
+ description: 'Tag for filtering a library for space',
+ id: 'gui.libraryTags.space'
+ },
+ sports: {
+ defaultMessage: 'Sports',
+ description: 'Tag for filtering a library for sports',
+ id: 'gui.libraryTags.sports'
+ },
+ underwater: {
+ defaultMessage: 'Underwater',
+ description: 'Tag for filtering a library for underwater',
+ id: 'gui.libraryTags.underwater'
+ },
+ voice: {
+ defaultMessage: 'Voice',
+ description: 'Tag for filtering a library for voice',
+ id: 'gui.libraryTags.voice'
+ },
+ wacky: {
+ defaultMessage: 'Wacky',
+ description: 'Tag for filtering a library for wacky',
+ id: 'gui.libraryTags.wacky'
+ }
+});
diff --git a/src/lib/locale-utils.js b/src/lib/locale-utils.js
new file mode 100644
index 00000000000..571186b6c0e
--- /dev/null
+++ b/src/lib/locale-utils.js
@@ -0,0 +1,27 @@
+// TODO: this probably should be coming from scratch-l10n
+// Tracking in https://github.com/LLK/scratch-l10n/issues/32
+const rtlLocales = ['he'];
+
+const wideLocales = [
+ 'ab',
+ 'ca',
+ 'de',
+ 'el',
+ 'it',
+ 'ja',
+ 'ja-Hira',
+ 'ko',
+ 'hu',
+ 'ru',
+ 'vi'
+];
+
+const isWideLocale = locale => (
+ wideLocales.indexOf(locale) !== -1
+);
+
+export {
+ rtlLocales,
+ wideLocales,
+ isWideLocale
+};
diff --git a/src/lib/monitor-adapter.js b/src/lib/monitor-adapter.js
index d7bdd304e3d..be0b1db7199 100644
--- a/src/lib/monitor-adapter.js
+++ b/src/lib/monitor-adapter.js
@@ -11,10 +11,13 @@ const isUndefined = a => typeof a === 'undefined';
* @param {string} block.opcode - The opcode of the monitor
* @param {object} block.params - Extra params to the monitor block
* @param {string|number|Array} block.value - The monitor value
+ * @param {VirtualMachine} block.vm - the VM instance which owns the block
* @return {object} The adapted monitor with label and category
*/
-export default function ({id, spriteName, opcode, params, value}) {
- let {label, category, labelFn} = OpcodeLabels(opcode);
+export default function ({id, spriteName, opcode, params, value, vm}) {
+ // Extension monitors get their labels from the Runtime through `getLabelForOpcode`.
+ // Other monitors' labels are hard-coded in `OpcodeLabels`.
+ let {label, category, labelFn} = (vm && vm.runtime.getLabelForOpcode(opcode)) || OpcodeLabels(opcode);
// Use labelFn if provided for dynamic labelling (e.g. variables)
if (!isUndefined(labelFn)) label = labelFn(params);
diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx
index cb8501af42a..c045dc0eebe 100644
--- a/src/lib/project-loader-hoc.jsx
+++ b/src/lib/project-loader-hoc.jsx
@@ -1,5 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
+import {injectIntl, intlShape} from 'react-intl';
+
+import {setProjectId} from '../reducers/project-id';
import analytics from './analytics';
import log from './log';
@@ -19,14 +23,27 @@ const ProjectLoaderHOC = function (WrappedComponent) {
projectData: null,
fetchingProject: false
};
- }
- componentDidMount () {
- if (this.props.projectId || this.props.projectId === 0) {
- this.updateProject(this.props.projectId);
+ storage.setProjectHost(props.projectHost);
+ storage.setAssetHost(props.assetHost);
+ storage.setTranslatorFunction(props.intl.formatMessage);
+ props.setProjectId(props.projectId);
+ if (
+ props.projectId !== '' &&
+ props.projectId !== null &&
+ typeof props.projectId !== 'undefined'
+ ) {
+ this.updateProject(props.projectId);
}
}
componentWillUpdate (nextProps) {
+ if (this.props.projectHost !== nextProps.projectHost) {
+ storage.setProjectHost(nextProps.projectHost);
+ }
+ if (this.props.assetHost !== nextProps.assetHost) {
+ storage.setAssetHost(nextProps.assetHost);
+ }
if (this.props.projectId !== nextProps.projectId) {
+ this.props.setProjectId(nextProps.projectId);
this.setState({fetchingProject: true}, () => {
this.updateProject(nextProps.projectId);
});
@@ -53,7 +70,12 @@ const ProjectLoaderHOC = function (WrappedComponent) {
}
render () {
const {
- projectId, // eslint-disable-line no-unused-vars
+ /* eslint-disable no-unused-vars */
+ assetHost,
+ projectHost,
+ projectId,
+ setProjectId: setProjectIdProp,
+ /* eslint-enable no-unused-vars */
...componentProps
} = this.props;
if (!this.state.projectData) return null;
@@ -67,13 +89,25 @@ const ProjectLoaderHOC = function (WrappedComponent) {
}
}
ProjectLoaderComponent.propTypes = {
- projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
+ assetHost: PropTypes.string,
+ intl: intlShape.isRequired,
+ projectHost: PropTypes.string,
+ projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ setProjectId: PropTypes.func
};
ProjectLoaderComponent.defaultProps = {
+ assetHost: 'https://assets.scratch.mit.edu',
+ projectHost: 'https://projects.scratch.mit.edu',
projectId: 0
};
- return ProjectLoaderComponent;
+ const mapStateToProps = () => ({});
+
+ const mapDispatchToProps = dispatch => ({
+ setProjectId: id => dispatch(setProjectId(id))
+ });
+
+ return injectIntl(connect(mapStateToProps, mapDispatchToProps)(ProjectLoaderComponent));
};
export {
diff --git a/src/lib/screen-utils.js b/src/lib/screen-utils.js
index e233993e261..d6120178412 100644
--- a/src/lib/screen-utils.js
+++ b/src/lib/screen-utils.js
@@ -10,8 +10,13 @@ import layout, {STAGE_DISPLAY_SCALES, STAGE_SIZE_MODES, STAGE_DISPLAY_SIZES} fro
*/
const STAGE_DIMENSION_DEFAULTS = {
- spacingBorderAdjustment: 9,
- menuHeightAdjustment: 40
+ // referencing css/units.css,
+ // spacingBorderAdjustment = 2 * $full-screen-top-bottom-margin +
+ // 2 * $full-screen-border-width
+ fullScreenSpacingBorderAdjustment: 12,
+ // referencing css/units.css,
+ // menuHeightAdjustment = $stage-menu-height
+ menuHeightAdjustment: 44
};
/**
@@ -48,7 +53,7 @@ const getStageDimensions = (stageSize, isFullScreen) => {
if (isFullScreen) {
stageDimensions.height = window.innerHeight -
STAGE_DIMENSION_DEFAULTS.menuHeightAdjustment -
- STAGE_DIMENSION_DEFAULTS.spacingBorderAdjustment;
+ STAGE_DIMENSION_DEFAULTS.fullScreenSpacingBorderAdjustment;
stageDimensions.width = stageDimensions.height + (stageDimensions.height / 3);
@@ -67,7 +72,29 @@ const getStageDimensions = (stageSize, isFullScreen) => {
return stageDimensions;
};
+/**
+ * Take a pair of sizes for the stage (a target height and width and a default height and width),
+ * calculate the ratio between them, and return a CSS transform to scale to that ratio.
+ * @param {object} sizeInfo An object containing dimensions of the target and default stage sizes.
+ * @param {number} sizeInfo.width The target width
+ * @param {number} sizeInfo.height The target height
+ * @param {number} sizeInfo.widthDefault The default width
+ * @param {number} sizeInfo.heightDefault The default height
+ * @returns {object} the CSS transform
+ */
+const stageSizeToTransform = ({width, height, widthDefault, heightDefault}) => {
+ const scaleX = width / widthDefault;
+ const scaleY = height / heightDefault;
+ if (scaleX === 1 && scaleY === 1) {
+ // Do not set a transform if the scale is 1 because
+ // it messes up `position: fixed` elements like the context menu.
+ return;
+ }
+ return {transform: `scale(${scaleX},${scaleY})`};
+};
+
export {
getStageDimensions,
- resolveStageSize
+ resolveStageSize,
+ stageSizeToTransform
};
diff --git a/src/lib/shared-messages.js b/src/lib/shared-messages.js
new file mode 100644
index 00000000000..0de3e342de2
--- /dev/null
+++ b/src/lib/shared-messages.js
@@ -0,0 +1,24 @@
+import {defineMessages} from 'react-intl';
+
+export default defineMessages({
+ backdrop: {
+ defaultMessage: 'backdrop{index}',
+ description: 'Default name for a new backdrop, scratch will automatically adjust the number if necessary',
+ id: 'gui.sharedMessages.backdrop'
+ },
+ costume: {
+ defaultMessage: 'costume{index}',
+ description: 'Default name for a new costume, scratch will automatically adjust the number if necessary',
+ id: 'gui.sharedMessages.costume'
+ },
+ sprite: {
+ defaultMessage: 'Sprite{index}',
+ description: 'Default name for a new sprite, scratch will automatically adjust the number if necessary',
+ id: 'gui.sharedMessages.sprite'
+ },
+ pop: {
+ defaultMessage: 'pop',
+ description: 'Name of the pop sound, the default sound added to a sprite',
+ id: 'gui.sharedMessages.pop'
+ }
+});
diff --git a/src/lib/storage.js b/src/lib/storage.js
index be7bcef1e6d..b0634d9c3cf 100644
--- a/src/lib/storage.js
+++ b/src/lib/storage.js
@@ -1,9 +1,6 @@
import ScratchStorage from 'scratch-storage';
-import defaultProjectAssets from './default-project';
-
-const PROJECT_SERVER = 'https://projects.scratch.mit.edu';
-const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu';
+import defaultProject from './default-project';
/**
* Wrapper for ScratchStorage which adds default web sources.
@@ -12,23 +9,52 @@ const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu';
class Storage extends ScratchStorage {
constructor () {
super();
- this.addWebSource(
+ this.cacheDefaultProject();
+ this.addWebStore(
[this.AssetType.Project],
- projectAsset => {
- const [projectId, revision] = projectAsset.assetId.split('.');
- return revision ?
- `${PROJECT_SERVER}/internalapi/project/${projectId}/get/${revision}` :
- `${PROJECT_SERVER}/internalapi/project/${projectId}/get/`;
- }
+ this.getProjectGetConfig.bind(this),
+ this.getProjectCreateConfig.bind(this),
+ this.getProjectUpdateConfig.bind(this)
);
- this.addWebSource(
+ this.addWebStore(
[this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound],
- asset => `${ASSET_SERVER}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`
+ this.getAssetGetConfig.bind(this)
);
- this.addWebSource(
+ this.addWebStore(
[this.AssetType.Sound],
asset => `static/extension-assets/scratch3_music/${asset.assetId}.${asset.dataFormat}`
);
+ }
+ setProjectHost (projectHost) {
+ this.projectHost = projectHost;
+ }
+ getProjectGetConfig (projectAsset) {
+ return `${this.projectHost}/internalapi/project/${projectAsset.assetId}/get/`;
+ }
+ getProjectCreateConfig () {
+ return {
+ url: `${this.projectHost}/`,
+ withCredentials: true
+ };
+ }
+ getProjectUpdateConfig (projectAsset) {
+ return {
+ url: `${this.projectHost}/${projectAsset.assetId}`,
+ withCredentials: true
+ };
+ }
+ setAssetHost (assetHost) {
+ this.assetHost = assetHost;
+ }
+ getAssetGetConfig (asset) {
+ return `${this.assetHost}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`;
+ }
+ setTranslatorFunction (translator) {
+ this.translator = translator;
+ this.cacheDefaultProject();
+ }
+ cacheDefaultProject () {
+ const defaultProjectAssets = defaultProject(this.translator);
defaultProjectAssets.forEach(asset => this.cache(
this.AssetType[asset.assetType],
this.DataFormat[asset.dataFormat],
diff --git a/src/lib/titled-hoc.jsx b/src/lib/titled-hoc.jsx
new file mode 100644
index 00000000000..2534d96bd61
--- /dev/null
+++ b/src/lib/titled-hoc.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import bindAll from 'lodash.bindall';
+import {defineMessages, intlShape, injectIntl} from 'react-intl';
+
+const messages = defineMessages({
+ defaultProjectTitle: {
+ id: 'gui.gui.defaultProjectTitle',
+ description: 'Default title for project',
+ defaultMessage: 'Scratch Project'
+ }
+});
+
+/* Higher Order Component to get and set the project title
+ * @param {React.Component} WrappedComponent component to receive project title related props
+ * @returns {React.Component} component with project loading behavior
+ */
+const TitledHOC = function (WrappedComponent) {
+ class TitledComponent extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleUpdateProjectTitle'
+ ]);
+ this.state = {
+ projectTitle: this.props.intl.formatMessage(messages.defaultProjectTitle)
+ };
+ }
+ handleUpdateProjectTitle (newTitle) {
+ this.setState({projectTitle: newTitle});
+ }
+ render () {
+ return (
+
+ );
+ }
+ }
+
+ TitledComponent.propTypes = {
+ intl: intlShape.isRequired
+ };
+
+ // return TitledComponent;
+ const IntlTitledComponent = injectIntl(TitledComponent);
+ return IntlTitledComponent;
+
+};
+
+export {
+ TitledHOC as default
+};
diff --git a/src/lib/tutorial-from-url.js b/src/lib/tutorial-from-url.js
new file mode 100644
index 00000000000..c627b8db7ab
--- /dev/null
+++ b/src/lib/tutorial-from-url.js
@@ -0,0 +1,48 @@
+/**
+ * @fileoverview
+ * Utility function to detect tutorial id from query paramenter on the URL.
+ */
+
+import tutorials from './libraries/decks/index.jsx';
+import analytics from './analytics';
+
+/**
+ * Get the tutorial id from the given numerical id (representing the
+ * url id of the tutorial).
+ * @param {number} urlId The URL Id for the tutorial
+ * @returns {string} The string id for the tutorial, or null if the URL ID
+ * was not found.
+ */
+const getDeckIdFromUrlId = urlId => {
+ for (const deckId in tutorials) {
+ if (tutorials[deckId].urlId === urlId) {
+ analytics.event({
+ category: 'how-to',
+ action: 'load from url',
+ label: `${deckId}`
+ });
+ return deckId;
+ }
+ }
+ return null;
+};
+
+/**
+ * Check if there's a tutorial id provided as a query parameter in the URL.
+ * Return the corresponding tutorial id or null if not found.
+ * @return {string} The ID of the requested tutorial or null if no tutorial was
+ * requested or found.
+ */
+const detectTutorialId = () => {
+ if (window.location.search.indexOf('tutorial=') !== -1) {
+ const urlTutorialId = window.location.search.match(/(?:tutorial)=(\d+)/)[1];
+ if (urlTutorialId) {
+ return getDeckIdFromUrlId(Number(urlTutorialId));
+ }
+ }
+ return null;
+};
+
+export {
+ detectTutorialId
+};
diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx
index 3fd9e2096f1..6b8d1777792 100644
--- a/src/lib/vm-listener-hoc.jsx
+++ b/src/lib/vm-listener-hoc.jsx
@@ -8,6 +8,9 @@ import {connect} from 'react-redux';
import {updateTargets} from '../reducers/targets';
import {updateBlockDrag} from '../reducers/block-drag';
import {updateMonitors} from '../reducers/monitors';
+import {setRunningState, setTurboState} from '../reducers/vm-status';
+import {showAlert} from '../reducers/alerts';
+import {updateMicIndicator} from '../reducers/mic-indicator';
/*
* Higher Order Component to manage events emitted by the VM
@@ -31,12 +34,25 @@ const vmListenerHOC = function (WrappedComponent) {
this.props.vm.on('targetsUpdate', this.props.onTargetsUpdate);
this.props.vm.on('MONITORS_UPDATE', this.props.onMonitorsUpdate);
this.props.vm.on('BLOCK_DRAG_UPDATE', this.props.onBlockDragUpdate);
+ this.props.vm.on('TURBO_MODE_ON', this.props.onTurboModeOn);
+ this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff);
+ this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart);
+ this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop);
+ this.props.vm.on('PERIPHERAL_ERROR', this.props.onShowAlert);
+ this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate);
+
}
componentDidMount () {
if (this.props.attachKeyboardEvents) {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
}
+ this.props.vm.postIOData('userData', {username: this.props.username});
+ }
+ componentWillReceiveProps (newProps) {
+ if (newProps.username !== this.props.username) {
+ this.props.vm.postIOData('userData', {username: newProps.username});
+ }
}
componentWillUnmount () {
if (this.props.attachKeyboardEvents) {
@@ -72,11 +88,18 @@ const vmListenerHOC = function (WrappedComponent) {
const {
/* eslint-disable no-unused-vars */
attachKeyboardEvents,
+ username,
onBlockDragUpdate,
onKeyDown,
onKeyUp,
+ onMicListeningUpdate,
onMonitorsUpdate,
onTargetsUpdate,
+ onProjectRunStart,
+ onProjectRunStop,
+ onTurboModeOff,
+ onTurboModeOn,
+ onShowAlert,
/* eslint-enable no-unused-vars */
...props
} = this.props;
@@ -88,15 +111,24 @@ const vmListenerHOC = function (WrappedComponent) {
onBlockDragUpdate: PropTypes.func.isRequired,
onKeyDown: PropTypes.func,
onKeyUp: PropTypes.func,
+ onMicListeningUpdate: PropTypes.func.isRequired,
onMonitorsUpdate: PropTypes.func.isRequired,
+ onProjectRunStart: PropTypes.func.isRequired,
+ onProjectRunStop: PropTypes.func.isRequired,
+ onShowAlert: PropTypes.func.isRequired,
onTargetsUpdate: PropTypes.func.isRequired,
+ onTurboModeOff: PropTypes.func.isRequired,
+ onTurboModeOn: PropTypes.func.isRequired,
+ username: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
VMListener.defaultProps = {
attachKeyboardEvents: true
};
const mapStateToProps = state => ({
- vm: state.scratchGui.vm
+ vm: state.scratchGui.vm,
+ username: state.session && state.session.session && state.session.session.user ?
+ state.session.session.user.username : ''
});
const mapDispatchToProps = dispatch => ({
onTargetsUpdate: data => {
@@ -107,6 +139,16 @@ const vmListenerHOC = function (WrappedComponent) {
},
onBlockDragUpdate: areBlocksOverGui => {
dispatch(updateBlockDrag(areBlocksOverGui));
+ },
+ onProjectRunStart: () => dispatch(setRunningState(true)),
+ onProjectRunStop: () => dispatch(setRunningState(false)),
+ onTurboModeOn: () => dispatch(setTurboState(true)),
+ onTurboModeOff: () => dispatch(setTurboState(false)),
+ onShowAlert: data => {
+ dispatch(showAlert(data));
+ },
+ onMicListeningUpdate: listening => {
+ dispatch(updateMicIndicator(listening));
}
});
return connect(
diff --git a/src/playground/index.ejs b/src/playground/index.ejs
index 3d2394d662f..7a8bb16135e 100644
--- a/src/playground/index.ejs
+++ b/src/playground/index.ejs
@@ -3,6 +3,7 @@
+
<%= htmlWebpackPlugin.options.title %>
<% if (htmlWebpackPlugin.options.sentryConfig) { %>
diff --git a/src/playground/index.jsx b/src/playground/index.jsx
index da2be7a2783..ba0ada4da78 100644
--- a/src/playground/index.jsx
+++ b/src/playground/index.jsx
@@ -7,17 +7,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import analytics from '../lib/analytics';
-import GUI from '../containers/gui.jsx';
-import HashParserHOC from '../lib/hash-parser-hoc.jsx';
import AppStateHOC from '../lib/app-state-hoc.jsx';
+import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx';
+import supportedBrowser from '../lib/supported-browser';
import styles from './index.css';
-if (process.env.NODE_ENV === 'production' && typeof window === 'object') {
- // Warn before navigating away
- window.onbeforeunload = () => true;
-}
-
// Register "base" page view
analytics.pageview('/');
@@ -25,16 +20,15 @@ const appTarget = document.createElement('div');
appTarget.className = styles.app;
document.body.appendChild(appTarget);
-GUI.setAppElement(appTarget);
-const WrappedGui = HashParserHOC(AppStateHOC(GUI));
-
-// TODO a hack for testing the backpack, allow backpack host to be set by url param
-const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/);
-const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null;
-
-const backpackOptions = {
- visible: true,
- host: backpackHost
-};
-
-ReactDOM.render( , appTarget);
+if (supportedBrowser()) {
+ // require needed here to avoid importing unsupported browser-crashing code
+ // at the top level
+ require('./render-gui.jsx').default(appTarget);
+
+} else {
+ BrowserModalComponent.setAppElement(appTarget);
+ const WrappedBrowserModalComponent = AppStateHOC(BrowserModalComponent, true /* localesOnly */);
+ const handleBack = () => {};
+ // eslint-disable-next-line react/jsx-no-bind
+ ReactDOM.render( , appTarget);
+}
diff --git a/src/playground/player.jsx b/src/playground/player.jsx
index a0894fe39de..e236de667eb 100644
--- a/src/playground/player.jsx
+++ b/src/playground/player.jsx
@@ -8,6 +8,7 @@ import Box from '../components/box/box.jsx';
import GUI from '../containers/gui.jsx';
import HashParserHOC from '../lib/hash-parser-hoc.jsx';
import AppStateHOC from '../lib/app-state-hoc.jsx';
+import TitledHOC from '../lib/titled-hoc.jsx';
import {setPlayer} from '../reducers/mode';
@@ -18,7 +19,7 @@ if (process.env.NODE_ENV === 'production' && typeof window === 'object') {
import styles from './player.css';
-const Player = ({isPlayerOnly, onSeeInside}) => (
+const Player = ({isPlayerOnly, onSeeInside, projectId}) => (
(
);
Player.propTypes = {
isPlayerOnly: PropTypes.bool,
- onSeeInside: PropTypes.func
+ onSeeInside: PropTypes.func,
+ projectId: PropTypes.string
};
const mapStateToProps = state => ({
@@ -46,7 +49,7 @@ const mapDispatchToProps = dispatch => ({
});
const ConnectedPlayer = connect(mapStateToProps, mapDispatchToProps)(Player);
-const WrappedPlayer = HashParserHOC(AppStateHOC(ConnectedPlayer));
+const WrappedPlayer = HashParserHOC(AppStateHOC(TitledHOC(ConnectedPlayer)));
const appTarget = document.createElement('div');
document.body.appendChild(appTarget);
diff --git a/src/playground/render-gui.jsx b/src/playground/render-gui.jsx
new file mode 100644
index 00000000000..562e57eac9c
--- /dev/null
+++ b/src/playground/render-gui.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import AppStateHOC from '../lib/app-state-hoc.jsx';
+import GUI from '../containers/gui.jsx';
+import HashParserHOC from '../lib/hash-parser-hoc.jsx';
+import TitledHOC from '../lib/titled-hoc.jsx';
+
+/*
+ * Render the GUI playground. This is a separate function because importing anything
+ * that instantiates the VM causes unsupported browsers to crash
+ * {object} appTarget - the DOM element to render to
+ */
+export default appTarget => {
+ GUI.setAppElement(appTarget);
+ const WrappedGui = HashParserHOC(AppStateHOC(TitledHOC(GUI)));
+
+ // TODO a hack for testing the backpack, allow backpack host to be set by url param
+ const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/);
+ const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null;
+
+ const backpackOptions = {
+ visible: true,
+ host: backpackHost
+ };
+ if (process.env.NODE_ENV === 'production' && typeof window === 'object') {
+ // Warn before navigating away
+ window.onbeforeunload = () => true;
+ }
+
+ ReactDOM.render( , appTarget);
+};
diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js
new file mode 100644
index 00000000000..fefc97b3732
--- /dev/null
+++ b/src/reducers/alerts.js
@@ -0,0 +1,79 @@
+import extensionData from '../lib/libraries/extensions/index.jsx';
+
+const CLOSE_ALERT = 'scratch-gui/alerts/CLOSE_ALERT';
+const SHOW_ALERT = 'scratch-gui/alerts/SHOW_ALERT';
+
+const initialState = {
+ visible: true,
+ alertsList: []
+};
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+ switch (action.type) {
+ case SHOW_ALERT: {
+ const newList = state.alertsList.slice();
+ const newAlert = {message: action.data.message};
+ const extensionId = action.data.extensionId;
+ if (extensionId) { // if it's an extension
+ const extension = extensionData.find(ext => ext.extensionId === extensionId);
+ if (extension && extension.name) {
+ // TODO: is this the right place to assemble this message?
+ newAlert.message = `${newAlert.message} ${extension.name}.`;
+ }
+ if (extension && extension.smallPeripheralImage) {
+ newAlert.iconURL = extension.smallPeripheralImage;
+ }
+ }
+ // TODO: add cases for other kinds of alerts here?
+ newList.push(newAlert);
+ return Object.assign({}, state, {
+ alertsList: newList
+ });
+ }
+ case CLOSE_ALERT: {
+ const newList = state.alertsList.slice();
+ newList.splice(action.index, 1);
+ return Object.assign({}, state, {
+ alertsList: newList
+ });
+ }
+ default:
+ return state;
+ }
+};
+
+/**
+ * Function to close an alert with the given index.
+ *
+ * @param {object} index - the index of the alert to close.
+ * @return {object} - an object to be passed to the reducer.
+ */
+const closeAlert = function (index) {
+ return {
+ type: CLOSE_ALERT,
+ index
+ };
+};
+
+/**
+ * Function to show an alert with the given input data.
+ *
+ * @param {object} data - data for the alert
+ * @param {string} data.message - message for the alert
+ * @param {string} data.extensionId - extension ID for the alert
+ * @return {object} - an object to be passed to the reducer.
+ */
+const showAlert = function (data) {
+ return {
+ type: SHOW_ALERT,
+ data
+ };
+};
+
+export {
+ reducer as default,
+ initialState as alertsInitialState,
+ closeAlert,
+ showAlert
+};
diff --git a/src/reducers/cards.js b/src/reducers/cards.js
index 6f4add89d20..37319cbea06 100644
--- a/src/reducers/cards.js
+++ b/src/reducers/cards.js
@@ -1,3 +1,5 @@
+import analytics from '../lib/analytics';
+
import decks from '../lib/libraries/decks/index.jsx';
const CLOSE_CARDS = 'scratch-gui/cards/CLOSE_CARDS';
@@ -14,8 +16,8 @@ const initialState = {
content: decks,
activeDeckId: null,
step: 0,
- x: 292,
- y: 365,
+ x: 0,
+ y: 0,
dragging: false
};
@@ -38,6 +40,11 @@ const reducer = function (state, action) {
});
case NEXT_STEP:
if (state.activeDeckId !== null) {
+ analytics.event({
+ category: 'how-to',
+ action: 'next step',
+ label: `${state.activeDeckId} - ${state.step}`
+ });
return Object.assign({}, state, {
step: state.step + 1
});
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index 7d98ae2f3f7..bce112a1c32 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -1,4 +1,5 @@
import {applyMiddleware, compose, combineReducers} from 'redux';
+import alertsReducer, {alertsInitialState} from './alerts';
import assetDragReducer, {assetDragInitialState} from './asset-drag';
import cardsReducer, {cardsInitialState} from './cards';
import colorPickerReducer, {colorPickerInitialState} from './color-picker';
@@ -7,19 +8,27 @@ import blockDragReducer, {blockDragInitialState} from './block-drag';
import editorTabReducer, {editorTabInitialState} from './editor-tab';
import hoveredTargetReducer, {hoveredTargetInitialState} from './hovered-target';
import menuReducer, {menuInitialState} from './menus';
+import micIndicatorReducer, {micIndicatorInitialState} from './mic-indicator';
import modalReducer, {modalsInitialState} from './modals';
import modeReducer, {modeInitialState} from './mode';
import monitorReducer, {monitorsInitialState} from './monitors';
import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout';
+import projectIdReducer, {projectIdInitialState} from './project-id';
+import projectTitleReducer, {projectTitleInitialState} from './project-title';
+import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion';
import stageSizeReducer, {stageSizeInitialState} from './stage-size';
import targetReducer, {targetsInitialState} from './targets';
import toolboxReducer, {toolboxInitialState} from './toolbox';
import vmReducer, {vmInitialState} from './vm';
+import vmStatusReducer, {vmStatusInitialState} from './vm-status';
import throttle from 'redux-throttle';
+import decks from '../lib/libraries/decks/index.jsx';
+
const guiMiddleware = compose(applyMiddleware(throttle(300, {leading: true, trailing: true})));
const guiInitialState = {
+ alerts: alertsInitialState,
assetDrag: assetDragInitialState,
blockDrag: blockDragInitialState,
cards: cardsInitialState,
@@ -30,12 +39,17 @@ const guiInitialState = {
hoveredTarget: hoveredTargetInitialState,
stageSize: stageSizeInitialState,
menus: menuInitialState,
+ micIndicator: micIndicatorInitialState,
modals: modalsInitialState,
monitors: monitorsInitialState,
monitorLayout: monitorLayoutInitialState,
+ projectId: projectIdInitialState,
+ projectTitle: projectTitleInitialState,
+ restoreDeletion: restoreDeletionInitialState,
targets: targetsInitialState,
toolbox: toolboxInitialState,
- vm: vmInitialState
+ vm: vmInitialState,
+ vmStatus: vmStatusInitialState
};
const initPlayer = function (currentState) {
@@ -59,7 +73,29 @@ const initFullScreen = function (currentState) {
);
};
+const initTutorialCard = function (currentState, deckId) {
+ return Object.assign(
+ {},
+ currentState,
+ {
+ modals: {
+ previewInfo: false
+ },
+ cards: {
+ visible: true,
+ content: decks,
+ activeDeckId: deckId,
+ step: 0,
+ x: 0,
+ y: 0,
+ dragging: false
+ }
+ }
+ );
+};
+
const guiReducer = combineReducers({
+ alerts: alertsReducer,
assetDrag: assetDragReducer,
blockDrag: blockDragReducer,
cards: cardsReducer,
@@ -70,12 +106,17 @@ const guiReducer = combineReducers({
hoveredTarget: hoveredTargetReducer,
stageSize: stageSizeReducer,
menus: menuReducer,
+ micIndicator: micIndicatorReducer,
modals: modalReducer,
monitors: monitorReducer,
monitorLayout: monitorLayoutReducer,
+ projectId: projectIdReducer,
+ projectTitle: projectTitleReducer,
+ restoreDeletion: restoreDeletionReducer,
targets: targetReducer,
toolbox: toolboxReducer,
- vm: vmReducer
+ vm: vmReducer,
+ vmStatus: vmStatusReducer
});
export {
@@ -83,5 +124,6 @@ export {
guiInitialState,
guiMiddleware,
initFullScreen,
- initPlayer
+ initPlayer,
+ initTutorialCard
};
diff --git a/src/reducers/locales.js b/src/reducers/locales.js
index fda8f9dc36d..e2b7fed6ece 100644
--- a/src/reducers/locales.js
+++ b/src/reducers/locales.js
@@ -2,6 +2,7 @@ import {addLocaleData} from 'react-intl';
import {localeData} from 'scratch-l10n';
import editorMessages from 'scratch-l10n/locales/editor-msgs';
+import {rtlLocales} from '../lib/locale-utils';
addLocaleData(localeData);
@@ -9,6 +10,7 @@ const UPDATE_LOCALES = 'scratch-gui/locales/UPDATE_LOCALES';
const SELECT_LOCALE = 'scratch-gui/locales/SELECT_LOCALE';
const initialState = {
+ isRtl: false,
locale: 'en',
messagesByLocale: editorMessages,
messages: editorMessages.en
@@ -19,12 +21,14 @@ const reducer = function (state, action) {
switch (action.type) {
case SELECT_LOCALE:
return Object.assign({}, state, {
+ isRtl: rtlLocales.indexOf(action.locale) !== -1,
locale: action.locale,
messagesByLocale: state.messagesByLocale,
messages: state.messagesByLocale[action.locale]
});
case UPDATE_LOCALES:
return Object.assign({}, state, {
+ isRtl: state.isRtl,
locale: state.locale,
messagesByLocale: action.messagesByLocale,
messages: action.messagesByLocale[state.locale]
@@ -53,6 +57,7 @@ const initLocale = function (currentState, locale) {
{},
currentState,
{
+ isRtl: rtlLocales.indexOf(locale) !== -1,
locale: locale,
messagesByLocale: currentState.messagesByLocale,
messages: currentState.messagesByLocale[locale]
diff --git a/src/reducers/menus.js b/src/reducers/menus.js
index 6693160c177..4cc494c60b8 100644
--- a/src/reducers/menus.js
+++ b/src/reducers/menus.js
@@ -1,15 +1,19 @@
const OPEN_MENU = 'scratch-gui/menus/OPEN_MENU';
const CLOSE_MENU = 'scratch-gui/menus/CLOSE_MENU';
+const MENU_ACCOUNT = 'accountMenu';
const MENU_FILE = 'fileMenu';
const MENU_EDIT = 'editMenu';
const MENU_LANGUAGE = 'languageMenu';
+const MENU_LOGIN = 'loginMenu';
const initialState = {
+ [MENU_ACCOUNT]: false,
[MENU_FILE]: false,
[MENU_EDIT]: false,
- [MENU_LANGUAGE]: false
+ [MENU_LANGUAGE]: false,
+ [MENU_LOGIN]: false
};
const reducer = function (state, action) {
@@ -35,6 +39,9 @@ const closeMenu = menu => ({
type: CLOSE_MENU,
menu: menu
});
+const openAccountMenu = () => openMenu(MENU_ACCOUNT);
+const closeAccountMenu = () => closeMenu(MENU_ACCOUNT);
+const accountMenuOpen = state => state.scratchGui.menus[MENU_ACCOUNT];
const openFileMenu = () => openMenu(MENU_FILE);
const closeFileMenu = () => closeMenu(MENU_FILE);
const fileMenuOpen = state => state.scratchGui.menus[MENU_FILE];
@@ -44,17 +51,26 @@ const editMenuOpen = state => state.scratchGui.menus[MENU_EDIT];
const openLanguageMenu = () => openMenu(MENU_LANGUAGE);
const closeLanguageMenu = () => closeMenu(MENU_LANGUAGE);
const languageMenuOpen = state => state.scratchGui.menus[MENU_LANGUAGE];
+const openLoginMenu = () => openMenu(MENU_LOGIN);
+const closeLoginMenu = () => closeMenu(MENU_LOGIN);
+const loginMenuOpen = state => state.scratchGui.menus[MENU_LOGIN];
export {
reducer as default,
initialState as menuInitialState,
+ openAccountMenu,
+ closeAccountMenu,
+ accountMenuOpen,
openFileMenu,
closeFileMenu,
+ fileMenuOpen,
openEditMenu,
closeEditMenu,
+ editMenuOpen,
openLanguageMenu,
closeLanguageMenu,
- fileMenuOpen,
- editMenuOpen,
- languageMenuOpen
+ languageMenuOpen,
+ openLoginMenu,
+ closeLoginMenu,
+ loginMenuOpen
};
diff --git a/src/reducers/mic-indicator.js b/src/reducers/mic-indicator.js
new file mode 100644
index 00000000000..2548abbb41e
--- /dev/null
+++ b/src/reducers/mic-indicator.js
@@ -0,0 +1,26 @@
+const UPDATE = 'scratch-gui/mic-indicator/UPDATE';
+
+const initialState = false;
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+ switch (action.type) {
+ case UPDATE:
+ return action.visible;
+ default:
+ return state;
+ }
+};
+
+const updateMicIndicator = function (visible) {
+ return {
+ type: UPDATE,
+ visible: visible
+ };
+};
+
+export {
+ reducer as default,
+ initialState as micIndicatorInitialState,
+ updateMicIndicator
+};
diff --git a/src/reducers/project-id.js b/src/reducers/project-id.js
new file mode 100644
index 00000000000..9045873c74e
--- /dev/null
+++ b/src/reducers/project-id.js
@@ -0,0 +1,27 @@
+const SET_PROJECT_ID = 'scratch-gui/project-id/SET_PROJECT_ID';
+
+const initialState = null;
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+
+ switch (action.type) {
+ case SET_PROJECT_ID:
+ return action.id;
+ default:
+ return state;
+ }
+};
+
+const setProjectId = function (id) {
+ return {
+ type: SET_PROJECT_ID,
+ id: id
+ };
+};
+
+export {
+ reducer as default,
+ initialState as projectIdInitialState,
+ setProjectId
+};
diff --git a/src/reducers/project-title.js b/src/reducers/project-title.js
new file mode 100644
index 00000000000..09bf6c4eab4
--- /dev/null
+++ b/src/reducers/project-title.js
@@ -0,0 +1,25 @@
+const SET_PROJECT_TITLE = 'projectTitle/SET_PROJECT_TITLE';
+
+// we are initializing to a blank string instead of an actual title,
+// because it would be hard to localize here
+const initialState = '';
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+ switch (action.type) {
+ case SET_PROJECT_TITLE:
+ return action.title;
+ default:
+ return state;
+ }
+};
+const setProjectTitle = title => ({
+ type: SET_PROJECT_TITLE,
+ title: title
+});
+
+export {
+ reducer as default,
+ initialState as projectTitleInitialState,
+ setProjectTitle
+};
diff --git a/src/reducers/restore-deletion.js b/src/reducers/restore-deletion.js
new file mode 100644
index 00000000000..0c723fc0323
--- /dev/null
+++ b/src/reducers/restore-deletion.js
@@ -0,0 +1,33 @@
+const RESTORE_UPDATE = 'scratch-gui/restore-deletion/RESTORE_UPDATE';
+
+const initialState = {
+ restoreFun: null,
+ deletedItem: ''
+};
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+
+ switch (action.type) {
+ case RESTORE_UPDATE:
+ return Object.assign({}, state, action.state);
+ default:
+ return state;
+ }
+};
+
+const setRestore = function (state) {
+ return {
+ type: RESTORE_UPDATE,
+ state: {
+ restoreFun: state.restoreFun,
+ deletedItem: state.deletedItem
+ }
+ };
+};
+
+export {
+ reducer as default,
+ initialState as restoreDeletionInitialState,
+ setRestore
+};
diff --git a/src/reducers/vm-status.js b/src/reducers/vm-status.js
new file mode 100644
index 00000000000..8e965199232
--- /dev/null
+++ b/src/reducers/vm-status.js
@@ -0,0 +1,44 @@
+const SET_RUNNING_STATE = 'scratch-gui/vm-status/SET_RUNNING_STATE';
+const SET_TURBO_STATE = 'scratch-gui/vm-status/SET_TURBO_STATE';
+
+const initialState = {
+ running: false,
+ turbo: false
+};
+
+const reducer = function (state, action) {
+ if (typeof state === 'undefined') state = initialState;
+ switch (action.type) {
+ case SET_RUNNING_STATE:
+ return Object.assign({}, state, {
+ running: action.running
+ });
+ case SET_TURBO_STATE:
+ return Object.assign({}, state, {
+ turbo: action.turbo
+ });
+ default:
+ return state;
+ }
+};
+
+const setRunningState = function (running) {
+ return {
+ type: SET_RUNNING_STATE,
+ running: running
+ };
+};
+
+const setTurboState = function (turbo) {
+ return {
+ type: SET_TURBO_STATE,
+ turbo: turbo
+ };
+};
+
+export {
+ reducer as default,
+ initialState as vmStatusInitialState,
+ setRunningState,
+ setTurboState
+};
diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js
index ce2d6a533e3..2d979e44862 100644
--- a/test/helpers/selenium-helper.js
+++ b/test/helpers/selenium-helper.js
@@ -44,6 +44,9 @@ class SeleniumHelper {
args.push('--headless');
}
chromeCapabilities.set('chromeOptions', {args});
+ chromeCapabilities.setLoggingPrefs({
+ performance: 'ALL'
+ });
this.driver = new webdriver.Builder()
.forBrowser('chrome')
.withCapabilities(chromeCapabilities)
diff --git a/test/integration/backpack.test.js b/test/integration/backpack.test.js
index 9694e67ba2c..cfc78613ccd 100644
--- a/test/integration/backpack.test.js
+++ b/test/integration/backpack.test.js
@@ -24,7 +24,7 @@ describe('Working with the how-to library', () => {
test('Backpack is "Coming Soon" without backpack host param', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
// Check that the backpack header is visible and wrapped in a coming soon tooltip
await clickText('Backpack', '*[@data-for="backpack-tooltip"]');
const logs = await getLogs();
@@ -33,7 +33,7 @@ describe('Working with the how-to library', () => {
test('Backpack can be expanded with backpack host param', async () => {
await loadUri(`${uri}?backpack_host=some-value`);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
// Try activating the backpack from the costumes tab to make sure it isn't pushed off
await clickText('Costumes');
diff --git a/test/integration/blocks.test.js b/test/integration/blocks.test.js
index e7fcf0b827f..b3ec62e2f60 100644
--- a/test/integration/blocks.test.js
+++ b/test/integration/blocks.test.js
@@ -29,7 +29,7 @@ describe('Working with the blocks', () => {
test('Blocks report when clicked in the toolbox', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Code');
await clickText('Operators', scope.blocksTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
@@ -41,7 +41,7 @@ describe('Working with the blocks', () => {
test('Switching sprites updates the block menus', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Sound', scope.blocksTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
// "Meow" sound block should be visible
@@ -58,7 +58,7 @@ describe('Working with the blocks', () => {
test('Creating variables', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Code');
await clickText('Variables', scope.blocksTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
@@ -68,11 +68,11 @@ describe('Working with the blocks', () => {
await findByText('0', scope.reportedValue);
await clickText('Make a Variable');
- let el = await findByXpath("//input[@placeholder='']");
+ let el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('score');
await clickButton('OK');
await clickText('Make a Variable');
- el = await findByXpath("//input[@placeholder='']");
+ el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('second variable');
await clickButton('OK');
@@ -92,13 +92,13 @@ describe('Working with the blocks', () => {
test('Creating a list', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Code');
await clickText('Variables', scope.blocksTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickText('Make a List');
- let el = await findByXpath("//input[@placeholder='']");
+ let el = await findByXpath("//input[@name='New list name:']");
await el.sendKeys('list1');
await clickButton('OK');
@@ -127,7 +127,7 @@ describe('Working with the blocks', () => {
test('Custom procedures', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('My Blocks');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickText('Make a Block');
@@ -146,7 +146,7 @@ describe('Working with the blocks', () => {
test('Adding an extension', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickXpath('//button[@title="Add Extension"]');
await clickText('Pen');
diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js
index 23790842d8d..7aa3b969fc2 100644
--- a/test/integration/costumes.test.js
+++ b/test/integration/costumes.test.js
@@ -27,7 +27,7 @@ describe('Working with costumes', () => {
test('Adding a costume through the library', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
await clickXpath('//button[@aria-label="Choose a Costume"]');
const el = await findByXpath("//input[@placeholder='Search']");
@@ -40,7 +40,7 @@ describe('Working with costumes', () => {
test('Adding a costume by surprise button', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
@@ -53,7 +53,7 @@ describe('Working with costumes', () => {
test('Adding a costume by paint button', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
@@ -66,7 +66,7 @@ describe('Working with costumes', () => {
test('Duplicating a costume', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
await rightClickText('costume1', scope.costumesTab);
@@ -82,7 +82,7 @@ describe('Working with costumes', () => {
test('Adding a backdrop', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickXpath('//button[@aria-label="Choose a Backdrop"]');
const el = await findByXpath("//input[@placeholder='Search']");
await el.sendKeys('blue');
@@ -93,7 +93,7 @@ describe('Working with costumes', () => {
test('Converting bitmap/vector in paint editor', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
// Convert the first costume to bitmap.
@@ -117,7 +117,7 @@ describe('Working with costumes', () => {
test('Undo/redo in the paint editor', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
await clickText('costume1', scope.costumesTab);
await clickText('Convert to Bitmap', scope.costumesTab);
diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js
index f745dc24d2c..8408c88f39b 100644
--- a/test/integration/examples.test.js
+++ b/test/integration/examples.test.js
@@ -35,6 +35,14 @@ describe('player example', () => {
await clickXpath('//img[@title="Stop"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
+ const projectRequests = await driver.manage().logs()
+ .get('performance')
+ .then(pLogs => pLogs.map(log => JSON.parse(log.message).message)
+ .filter(m => m.method === 'Network.requestWillBeSent')
+ .map(m => m.params.request.url)
+ .filter(url => url === 'https://projects.scratch.mit.edu/internalapi/project/96708228/get/')
+ );
+ await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/internalapi/project/96708228/get/']);
});
});
@@ -58,6 +66,14 @@ describe('blocks example', () => {
await clickXpath('//img[@title="Stop"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
+ const projectRequests = await driver.manage().logs()
+ .get('performance')
+ .then(pLogs => pLogs.map(log => JSON.parse(log.message).message)
+ .filter(m => m.method === 'Network.requestWillBeSent')
+ .map(m => m.params.request.url)
+ .filter(url => url === 'https://projects.scratch.mit.edu/internalapi/project/96708228/get/')
+ );
+ await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/internalapi/project/96708228/get/']);
});
test('Change categories', async () => {
@@ -71,11 +87,11 @@ describe('blocks example', () => {
await clickText('Variables');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickText('Make a Variable');
- let el = await findByXpath("//input[@placeholder='']");
+ let el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('score');
await clickButton('OK');
await clickText('Make a Variable');
- el = await findByXpath("//input[@placeholder='']");
+ el = await findByXpath("//input[@name='New variable name:']");
await el.sendKeys('second variable');
await clickButton('OK');
const logs = await getLogs();
diff --git a/test/integration/how-tos.test.js b/test/integration/how-tos.test.js
index 5b9b76e4b74..dcdccb49947 100644
--- a/test/integration/how-tos.test.js
+++ b/test/integration/how-tos.test.js
@@ -25,7 +25,7 @@ describe('Working with the how-to library', () => {
test('Choosing a how-to', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
await clickXpath('//*[@aria-label="Tutorials"]');
await clickText('Getting Started'); // Modal should close
diff --git a/test/integration/localization.test.js b/test/integration/localization.test.js
index deaf1a78814..cf02aa86b1b 100644
--- a/test/integration/localization.test.js
+++ b/test/integration/localization.test.js
@@ -4,11 +4,9 @@ import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
- findByText,
getDriver,
getLogs,
- loadUri,
- scope
+ loadUri
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
@@ -24,24 +22,20 @@ describe('Localization', () => {
await driver.quit();
});
- // Skipped temporarily while the language selector is marked as
- // "Coming Soon"
- test.skip('Localization', async () => {
+ test('Localization', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
- await clickText('Code');
- await clickXpath('//button[@title="Add Extension"]');
- await clickText('Pen', scope.modal); // Modal closes
- await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
- await clickText('English');
+ await clickXpath('//button[@title="Try It"]');
+ await clickXpath('//*[@aria-label="language selector"]');
await clickText('Deutsch');
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh
- await clickText('Pen'); // will need to be updated when 'Pen' is translated
- // Make sure "Add Sprite" has changed to "Figur hinzufügen"
- await findByText('Figur hinzufügen');
- // Find the stamp block in German
- await findByText('Abdruck');
+ // Make sure the blocks are translating
+ await clickText('Fühlen'); // Sensing category in German
+ await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll
+ await clickText('Antwort'); // Find the "answer" block in German
+
+ // Change to the costumes tab to confirm other parts of the GUI are translating
+ await clickText('Kostüme');
const logs = await getLogs();
await expect(logs).toEqual([]);
diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js
index cb0477ec931..09023632e8a 100644
--- a/test/integration/project-loading.test.js
+++ b/test/integration/project-loading.test.js
@@ -38,7 +38,7 @@ describe('Loading scratch gui', () => {
const el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']");
const projectId = '96708228';
await el.sendKeys(`scratch.mit.edu/projects/${projectId}`);
- await clickXpath('//button[@title="viewproject"]');
+ await clickXpath('//button[@title="View Project"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -52,11 +52,11 @@ describe('Loading scratch gui', () => {
await clickText('View 2.0 Project');
let el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']");
await el.sendKeys('thisisnotaurl');
- await clickXpath('//button[@title="viewproject"]');
+ await clickXpath('//button[@title="View Project"]');
el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']");
await el.clear();
await el.sendKeys('scratch.mit.edu/projects/96708228');
- await clickXpath('//button[@title="viewproject"]');
+ await clickXpath('//button[@title="View Project"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
@@ -71,8 +71,7 @@ describe('Loading scratch gui', () => {
const projectId = '96708228';
await loadUri(`${uri}#${projectId}`);
- await clickXpath('//button[@title="tryit"]');
- await new Promise(resolve => setTimeout(resolve, 2000));
+ await new Promise(resolve => setTimeout(resolve, 3000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Stop"]');
@@ -93,7 +92,6 @@ describe('Loading scratch gui', () => {
.setSize(1920, 1080);
const projectId = '96708228';
await loadUri(`${uri}#${projectId}`);
- await clickXpath('//button[@title="tryit"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Full Screen Control"]');
await clickXpath('//img[@title="Go"]');
diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js
index aa3790f9508..ca1a3d275d8 100644
--- a/test/integration/sounds.test.js
+++ b/test/integration/sounds.test.js
@@ -27,14 +27,12 @@ describe('Working with sounds', () => {
test('Adding a sound through the library', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Sounds');
// Delete the sound
await rightClickText('Meow', scope.soundsTab);
await clickText('delete', scope.soundsTab);
- await driver.switchTo().alert()
- .accept();
// Add it back
await clickXpath('//button[@aria-label="Choose a Sound"]');
@@ -65,7 +63,7 @@ describe('Working with sounds', () => {
test('Adding a sound by surprise button', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Sounds');
const el = await findByXpath('//button[@aria-label="Choose a Sound"]');
await driver.actions().mouseMove(el)
@@ -78,7 +76,7 @@ describe('Working with sounds', () => {
test('Duplicating a sound', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Sounds');
await rightClickText('Meow', scope.soundsTab);
@@ -95,7 +93,7 @@ describe('Working with sounds', () => {
// Regression test for gui issue #1320
test('Switching sprites with different numbers of sounds', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
// Add a sound so this sprite has 2 sounds.
await clickText('Sounds');
diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js
index 816d06a1ebf..5b99efcb62e 100644
--- a/test/integration/sprites.test.js
+++ b/test/integration/sprites.test.js
@@ -28,7 +28,7 @@ describe('Working with sprites', () => {
test('Adding a sprite through the library', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await clickText('Costumes');
await clickXpath('//button[@aria-label="Choose a Sprite"]');
await clickText('Apple', scope.modal); // Closes modal
@@ -39,7 +39,7 @@ describe('Working with sprites', () => {
test('Adding a sprite by surprise button', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
await driver.actions().mouseMove(el)
.perform();
@@ -51,12 +51,10 @@ describe('Working with sprites', () => {
test('Deleting only sprite does not crash', async () => {
await loadUri(uri);
- await clickXpath('//button[@title="tryit"]');
+ await clickXpath('//button[@title="Try It"]');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await rightClickText('Sprite1', scope.spriteTile);
await clickText('delete', scope.spriteTile);
- await driver.switchTo().alert()
- .accept();
// Confirm that the stage has been switched to
await findByText('Stage selected: no motion blocks');
const logs = await getLogs();
diff --git a/test/smoke/browser.test.js b/test/smoke/browser.test.js
index b822cd1b165..941b3eef3bd 100644
--- a/test/smoke/browser.test.js
+++ b/test/smoke/browser.test.js
@@ -8,7 +8,7 @@ const {
// Make the default timeout longer, Sauce tests take ~30s
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 1000; // eslint-disable-line
-const SUPPORTED_MESSAGE = 'Welcome to the Scratch 3.0 Preview';
+const SUPPORTED_MESSAGE = 'Welcome to the Scratch 3.0 Beta';
const UNSUPPORTED_MESSAGE = 'Scratch 3.0 does not support Internet Explorer';
// Driver configs can be generated with the Sauce Platform Configurator
@@ -38,8 +38,7 @@ describe('Smoke tests on older browsers', () => {
return expect(isDisplayed).toEqual(true);
});
- // Safari 9 has always been blank screened due to lack of Intl polyfill
- test.skip('Safari 9 should not be unsupported', async () => {
+ test('Safari 9 should be supported', async () => {
const driverConfig = {
browserName: 'safari',
platform: 'OS X 10.11',
@@ -55,7 +54,7 @@ describe('Smoke tests on older browsers', () => {
return expect(isDisplayed).toEqual(true);
});
- test('Safari 10 should not be unsupported', async () => {
+ test('Safari 10 should be supported', async () => {
const driverConfig = {
browserName: 'safari',
platform: 'OS X 10.11',
diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
index 0279a418b36..93ebc2cf68c 100644
--- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
+++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
@@ -39,6 +39,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
title="Undo"
>
@@ -50,6 +51,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
title="Redo"
>
@@ -334,7 +336,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
@@ -30,16 +30,9 @@ exports[`SpriteSelectorItemComponent matches snapshot when given a number and de
>
5
-
-
{
@@ -73,17 +72,6 @@ describe('SpriteSelectorItemComponent', () => {
expect(onDeleteButtonClick).toHaveBeenCalled();
});
- test('creates a CostumeCanvas when a costume url is defined', () => {
- const wrapper = shallowWithIntl(getComponent());
- expect(wrapper.find(CostumeCanvas).exists()).toBe(true);
- });
-
- test('does not create a CostumeCanvas when a costume url is null', () => {
- costumeURL = null;
- const wrapper = shallowWithIntl(getComponent());
- expect(wrapper.find(CostumeCanvas).exists()).toBe(false);
- });
-
test('it has a context menu with delete menu item and callback', () => {
const wrapper = mountWithIntl(getComponent());
const contextMenu = wrapper.find('ContextMenu');
diff --git a/test/unit/containers/sprite-selector-item.test.jsx b/test/unit/containers/sprite-selector-item.test.jsx
index 85032603d4b..25200a1fb5f 100644
--- a/test/unit/containers/sprite-selector-item.test.jsx
+++ b/test/unit/containers/sprite-selector-item.test.jsx
@@ -4,6 +4,7 @@ import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux';
import SpriteSelectorItem from '../../../src/containers/sprite-selector-item';
+import {HAS_FONT_REGEXP} from '../../../src/containers/sprite-selector-item';
import CloseButton from '../../../src/components/close-button/close-button';
describe('SpriteSelectorItem Container', () => {
@@ -48,22 +49,19 @@ describe('SpriteSelectorItem Container', () => {
onDeleteButtonClick = jest.fn();
dispatchSetHoveredSprite = jest.fn();
selected = true;
- // Mock window.confirm() which is called when the close button is clicked.
- global.confirm = jest.fn(() => true);
});
- test('should confirm if the user really wants to delete the sprite', () => {
+ test('should delete the sprite', () => {
const wrapper = mountWithIntl(getContainer());
wrapper.find(CloseButton).simulate('click');
- expect(global.confirm).toHaveBeenCalled();
expect(onDeleteButtonClick).toHaveBeenCalledWith(1337);
});
- test('should not delete the sprite if the user cancels', () => {
- global.confirm = jest.fn(() => false);
- const wrapper = mountWithIntl(getContainer());
- wrapper.find(CloseButton).simulate('click');
- expect(global.confirm).toHaveBeenCalled();
- expect(onDeleteButtonClick).not.toHaveBeenCalled();
+ test('Has font regexp works', () => {
+ expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
+ expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
+ expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
+
+ expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy();
});
});
diff --git a/test/unit/util/project-loader-hoc.test.jsx b/test/unit/util/project-loader-hoc.test.jsx
index 502ac0f62d4..a0a51cc6610 100644
--- a/test/unit/util/project-loader-hoc.test.jsx
+++ b/test/unit/util/project-loader-hoc.test.jsx
@@ -1,23 +1,17 @@
import React from 'react';
+import configureStore from 'redux-mock-store';
import ProjectLoaderHOC from '../../../src/lib/project-loader-hoc.jsx';
import storage from '../../../src/lib/storage';
-import {mount} from 'enzyme';
+import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
jest.mock('react-ga');
describe('ProjectLoaderHOC', () => {
+ const mockStore = configureStore();
+ let store;
- test('when there is no id, it loads (default) project id 0', () => {
- const Component = ({projectData}) =>
{projectData}
;
- const WrappedComponent = ProjectLoaderHOC(Component);
- const originalLoad = storage.load;
- storage.load = jest.fn((type, id) => Promise.resolve(id));
- const mounted = mount(
);
- expect(mounted.props().projectId).toEqual(0);
- expect(storage.load).toHaveBeenCalledWith(
- storage.AssetType.Project, 0, storage.DataFormat.JSON
- );
- storage.load = originalLoad;
+ beforeEach(() => {
+ store = mockStore({scratchGui: {}});
});
test('when there is an id, it tries to load that project', () => {
@@ -25,7 +19,12 @@ describe('ProjectLoaderHOC', () => {
const WrappedComponent = ProjectLoaderHOC(Component);
const originalLoad = storage.load;
storage.load = jest.fn((type, id) => Promise.resolve({data: id}));
- const mounted = mount(
);
+ const mounted = mountWithIntl(
+
+ );
expect(mounted.props().projectId).toEqual('100');
expect(storage.load).toHaveBeenLastCalledWith(
storage.AssetType.Project, '100', storage.DataFormat.JSON
@@ -38,7 +37,7 @@ describe('ProjectLoaderHOC', () => {
const WrappedComponent = ProjectLoaderHOC(Component);
const originalLoad = storage.load;
storage.load = jest.fn(() => Promise.resolve(null));
- const mounted = mount(
);
+ const mounted = mountWithIntl(
);
storage.load = originalLoad;
const mountedDiv = mounted.find('div');
expect(mountedDiv.exists()).toEqual(false);
diff --git a/webpack.config.js b/webpack.config.js
index a44d90c48e2..1013bb87a08 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -177,7 +177,7 @@ module.exports = [
])
})
].concat(
- process.env.NODE_ENV === 'production' ? (
+ process.env.NODE_ENV === 'production' || process.env.BUILD_MODE === 'dist' ? (
// export as library
defaultsDeep({}, base, {
target: 'web',