Merge tag '0.1.0-prerelease.20190131130145'

NPM version 0.1.0-prerelease.20190131130145 deployed to production 1/31/2019
This commit is contained in:
Paul Kaplan committed 2019-02-04 09:45:26 -05:00
commit 11083c1c9d
249 files changed
+11756 -5476

No files matched your search

+5
View File
@@ -6,6 +6,11 @@
# File types which we know are binary
# Treat SVG files as binary so that their contents don't change due to line
# endings. The contents of SVGs must not change from the way they're stored
# on assets.scratch.mit.edu so that MD5 calculations don't change.
*.svg binary
# Prefer LF for most file types
*.frag text eol=lf
*.htm text eol=lf
+1
View File
@@ -4,6 +4,7 @@
# NPM
/node_modules
npm-*
/package-lock.json
# Testing
/.nyc_output
+10
View File
@@ -35,6 +35,7 @@ deploy:
- master
- develop
- smoke
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
email: $NPM_EMAIL
api_key: $NPM_TOKEN
@@ -44,6 +45,7 @@ deploy:
branch:
- master
- $PREVIEW_BRANCH
condition: $TRAVIS_EVENT_TYPE != cron
access_key_id: $AWS_ACCESS_KEY_ID
secret_access_key: $AWS_SECRET_ACCESS_KEY
bucket: $AWS_BUCKET_NAME
@@ -53,11 +55,19 @@ deploy:
- provider: script
on:
all_branches: true
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
- provider: script
on:
all_branches: true
condition: $TRAVIS_EVENT_TYPE != cron
script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
- provider: script
on:
branch: develop
condition: $TRAVIS_EVENT_TYPE == cron
skip_cleanup: true
script: npm run i18n:src && npm run i18n:push
after_deploy:
- 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all'
+1 -1
View File
@@ -13,7 +13,7 @@ npm install https://github.com/LLK/scratch-gui.git
```
If you want to edit/play yourself:
```bash
git clone git@github.com:LLK/scratch-gui.git
git clone https://github.com/LLK/scratch-gui.git
cd scratch-gui
npm install
```
+14 -11
View File
@@ -8,7 +8,8 @@
"clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist",
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"",
"prune": "./prune-gh-pages.sh",
"i18n:src": "babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/",
"i18n:push": "tx-push-src scratch-editor interface translations/en.json",
"i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/ && npm run i18n:push",
"start": "webpack-dev-server",
"test": "npm run test:lint && npm run test:unit && npm run build && npm run test:integration",
"test:integration": "jest --runInBand test[\\\\/]integration",
@@ -43,7 +44,7 @@
"babel-loader": "^8.0.4",
"base64-loader": "1.0.0",
"bowser": "1.9.4",
"chromedriver": "2.43.1",
"chromedriver": "2.44.1",
"classnames": "2.2.6",
"computed-style-to-inline-style": "3.0.0",
"copy-webpack-plugin": "^4.5.1",
@@ -55,7 +56,7 @@
"eslint": "^5.0.1",
"eslint-config-scratch": "^5.0.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-react": "^7.5.1",
"eslint-plugin-react": "7.11.1",
"file-loader": "2.0.0",
"get-float-time-domain-data": "0.1.0",
"get-user-media-promise": "1.1.4",
@@ -72,8 +73,10 @@
"lodash.isequal": "4.5.0",
"lodash.omit": "4.5.0",
"lodash.pick": "4.4.0",
"lodash.throttle": "4.0.1",
"minilog": "3.1.0",
"mkdirp": "^0.5.1",
"papaparse": "4.6.2",
"postcss-import": "^12.0.0",
"postcss-loader": "^3.0.0",
"postcss-simple-vars": "^5.0.1",
@@ -100,14 +103,14 @@
"redux-mock-store": "^1.2.3",
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20181023202904",
"scratch-blocks": "0.1.0-prerelease.1541684350",
"scratch-l10n": "3.0.20181108141849",
"scratch-paint": "0.2.0-prerelease.20181109181214",
"scratch-render": "0.1.0-prerelease.20181102130522",
"scratch-storage": "1.2.0",
"scratch-svg-renderer": "0.2.0-prerelease.20181101210634",
"scratch-vm": "0.2.0-prerelease.20181108103702",
"scratch-audio": "0.1.0-prerelease.20190114210212",
"scratch-blocks": "0.1.0-prerelease.1548885087",
"scratch-l10n": "3.1.20190130142816",
"scratch-paint": "0.2.0-prerelease.20190114205252",
"scratch-render": "0.1.0-prerelease.20190128154859",
"scratch-storage": "1.2.2",
"scratch-svg-renderer": "0.2.0-prerelease.20190125192231",
"scratch-vm": "0.2.0-prerelease.20190130160436",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
+2 -1
View File
@@ -1,4 +1,5 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/z-index.css";
$main-button-size: 2.75rem;
@@ -153,7 +154,7 @@ button::-moz-focus-inner {
.tooltip {
border: 1px solid hsla(0, 0%, 0%, .1) !important;
border-radius: .25rem !important;
border-radius: $form-radius !important;
box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
z-index: $z-index-tooltip !important;
@@ -78,6 +78,9 @@ class ActionMenu extends React.Component {
return event => {
ReactTooltip.hide();
if (fn) fn(event);
// Blur the button so it does not keep focus after being clicked
// This prevents keyboard events from triggering the button
this.buttonRef.blur();
this.setState({forceHide: true, isOpen: false}, () => {
setTimeout(() => this.setState({forceHide: false}));
});
+61 -16
View File
@@ -4,29 +4,59 @@
.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);
justify-content: flex-start;
border-radius: $space;
padding-top: .875rem;
padding-bottom: .875rem;
padding-left: 1rem;
padding-right: 1rem;
margin-bottom: 7px;
min-height: 1.5rem;
}
.alert.warn {
background: #FFF0DF;
border: 1px solid #FF8C1A;
box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25);
}
.alert.success {
background: $extensions-light;
border: 1px solid $extensions-tertiary;
box-shadow: 0px 0px 0px 2px $extensions-light;
}
.alert-spinner {
self-align: center;
}
.icon-section {
min-width: 1.25rem;
min-height: 1.25rem;
display: flex;
padding-right: 1rem;
}
.alert-icon {
margin-right: 5px;
vertical-align: middle;
}
.alert-message {
color: #555;
font-weight: bold;
font-size: 12px;
line-height: 22pt;
width: 100%;
font-size: .8125rem;
line-height: .875rem;
display: flex;
align-items: center;
padding-right: .5rem;
}
.alert-buttons {
display: flex;
flex-direction: row;
}
.alert-close-button {
@@ -34,25 +64,40 @@
}
.alert-close-button-container {
margin-top: 7px;
margin-right: 4px;
outline-style:none;
outline-style: none;
width: 30px;
height: 30px;
align-self: center;
}
.connection-button {
.alert-connection-button {
min-height: 2rem;
width: 6.5rem;
padding: 0.55rem 0.9rem;
border-radius: 0.35rem;
background: #FF8C1A;
color: white;
font-weight: 700;
font-size: 0.77rem;
margin: 0.25rem;
border: none;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
margin-right: 13px;
align-self: stretch;
outline-style:none;
}
[dir="ltr"] .alert-connection-button {
margin-right: 13px;
}
[dir="rtl"] .alert-connection-button {
margin-left: 13px;
}
/* prevent last button in list from too much margin to edge of alert */
.alert-buttons > :last-child {
margin-left: 0;
margin-right: 0;
}
+110 -32
View File
@@ -1,62 +1,140 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import Box from '../box/box.jsx';
import CloseButton from '../close-button/close-button.jsx';
import Spinner from '../spinner/spinner.jsx';
import {AlertLevels} from '../../lib/alerts/index.jsx';
import styles from './alert.css';
const closeButtonColors = {
[AlertLevels.SUCCESS]: CloseButton.COLOR_GREEN,
[AlertLevels.WARN]: CloseButton.COLOR_ORANGE
};
const AlertComponent = ({
content,
closeButton,
extensionName,
iconSpinner,
iconURL,
message,
level,
showDownload,
showSaveNow,
onCloseAlert,
onDownload,
onSaveNow,
onReconnect,
showReconnect
}) => (
<Box
className={styles.alert}
className={classNames(styles.alert, styles[level])}
>
{/* TODO: implement Rtl handling */}
{(iconSpinner || iconURL) && (
<div className={styles.iconSection}>
{iconSpinner && (
<Spinner
className={styles.alertSpinner}
level={level}
/>
)}
{iconURL && (
<img
className={styles.alertIcon}
src={iconURL}
/>
)}
</div>
)}
<div className={styles.alertMessage}>
{iconURL ? (
<img
className={styles.alertIcon}
src={iconURL}
/>
) : null}
{message}
</div>
{showReconnect ? (
<button
className={styles.connectionButton}
onClick={onReconnect}
>
{extensionName ? (
<FormattedMessage
defaultMessage="Reconnect"
description="Button to reconnect the device"
id="gui.connection.reconnect"
defaultMessage="Scratch lost connection to {extensionName}."
description="Message indicating that an extension peripheral has been disconnected"
id="gui.alerts.lostPeripheralConnection"
values={{
extensionName: (
`${extensionName}`
)
}}
/>
</button>
) : null}
<Box
className={styles.alertCloseButtonContainer}
>
<CloseButton
className={styles.alertCloseButton}
color={CloseButton.COLOR_ORANGE}
size={CloseButton.SIZE_LARGE}
onClick={onCloseAlert}
/>
</Box>
) : content}
</div>
<div className={styles.alertButtons}>
{showSaveNow && (
<button
className={styles.alertConnectionButton}
onClick={onSaveNow}
>
<FormattedMessage
defaultMessage="Try Again"
description="Button to try saving again"
id="gui.alerts.tryAgain"
/>
</button>
)}
{showDownload && (
<button
className={styles.alertConnectionButton}
onClick={onDownload}
>
<FormattedMessage
defaultMessage="Download"
description="Button to download project locally"
id="gui.alerts.download"
/>
</button>
)}
{showReconnect && (
<button
className={styles.alertConnectionButton}
onClick={onReconnect}
>
<FormattedMessage
defaultMessage="Reconnect"
description="Button to reconnect the device"
id="gui.connection.reconnect"
/>
</button>
)}
{closeButton && (
<Box
className={styles.alertCloseButtonContainer}
>
<CloseButton
className={classNames(styles.alertCloseButton)}
color={closeButtonColors[level]}
size={CloseButton.SIZE_LARGE}
onClick={onCloseAlert}
/>
</Box>
)}
</div>
</Box>
);
AlertComponent.propTypes = {
closeButton: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
extensionName: PropTypes.string,
iconSpinner: PropTypes.bool,
iconURL: PropTypes.string,
message: PropTypes.string,
level: PropTypes.string,
onCloseAlert: PropTypes.func.isRequired,
onDownload: PropTypes.func,
onReconnect: PropTypes.func,
showReconnect: PropTypes.bool.isRequired
onSaveNow: PropTypes.func,
showDownload: PropTypes.func,
showReconnect: PropTypes.bool,
showSaveNow: PropTypes.bool
};
AlertComponent.defaultProps = {
level: AlertLevels.WARN
};
export default AlertComponent;
+4
View File
@@ -0,0 +1,4 @@
.alerts-inner-container {
min-width: 200px;
max-width: 548px;
}
+22 -11
View File
@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import Box from '../box/box.jsx';
import Alert from '../../containers/alert.jsx';
import styles from './alerts.css';
const AlertsComponent = ({
alertsList,
className,
@@ -13,17 +15,26 @@ const AlertsComponent = ({
bounds="parent"
className={className}
>
{alertsList.map((a, index) => (
<Alert
extensionId={a.extensionId}
iconURL={a.iconURL}
index={index}
key={index}
message={a.message}
showReconnect={a.showReconnect}
onCloseAlert={onCloseAlert}
/>
))}
<Box className={styles.alertsInnerContainer} >
{alertsList.map((a, index) => (
<Alert
closeButton={a.closeButton}
content={a.content}
extensionId={a.extensionId}
extensionName={a.extensionName}
iconSpinner={a.iconSpinner}
iconURL={a.iconURL}
index={index}
key={index}
level={a.level}
message={a.message}
showDownload={a.showDownload}
showReconnect={a.showReconnect}
showSaveNow={a.showSaveNow}
onCloseAlert={onCloseAlert}
/>
))}
</Box>
</Box>
);
+27
View File
@@ -0,0 +1,27 @@
@import "../../css/colors.css";
@import "../../css/units.css";
.inline-message {
color: $ui-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
display: flex;
justify-content: end;
align-items: center;
font-size: .8125rem;
}
.success {
color: $ui-white-dim;
}
.info {
color: $ui-white;
}
.warn {
color: $error-light;
}
.spinner {
margin-right: $space;
}
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Spinner from '../spinner/spinner.jsx';
import {AlertLevels} from '../../lib/alerts/index.jsx';
import styles from './inline-message.css';
const InlineMessageComponent = ({
content,
iconSpinner,
level
}) => (
<div
className={classNames(styles.inlineMessage, styles[level])}
>
{/* TODO: implement Rtl handling */}
{iconSpinner && (
<Spinner
small
className={styles.spinner}
level={'info'}
/>
)}
{content}
</div>
);
InlineMessageComponent.propTypes = {
content: PropTypes.element,
iconSpinner: PropTypes.bool,
level: PropTypes.string
};
InlineMessageComponent.defaultProps = {
level: AlertLevels.INFO
};
export default InlineMessageComponent;
+2 -2
View File
@@ -56,7 +56,7 @@ $fade-out-distance: 100px;
.list-item {
width: 5rem;
min-height: 5rem;
height: 5rem;
margin: 0.5rem auto;
}
@@ -72,6 +72,6 @@ $fade-out-distance: 100px;
.list-item.placeholder {
background: black;
background: white;
filter: opacity(15%) brightness(0%);
}
+21 -1
View File
@@ -12,6 +12,9 @@ $hover-scale: 2;
left: 0;
width: 100%;
height: 100%;
/* Force the browser to paint separately to avoid composite cost with waveform */
transform: translateZ(0);
}
.trim-background {
@@ -50,8 +53,25 @@ $hover-scale: 2;
border: 1px solid $red-tertiary;
}
.playhead-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
.playhead {
border: 1px solid $motion-primary;
/*
Even though playhead is just a line, it is 100% width (the width of the waveform)
so that we can use transform: translateX() using percentages.
*/
width: 100%;
border-left: 1px solid $motion-primary;
border-top: none;
border-bottom: none;
border-right: none;
}
.start-trim-line {
@@ -32,12 +32,14 @@ const AudioTrimmer = props => (
)}
{props.playhead ? (
<Box
className={classNames(styles.trimLine, styles.playhead)}
style={{
left: `${100 * props.playhead}%`
}}
/>
<div className={styles.playheadContainer}>
<div
className={classNames(styles.trimLine, styles.playhead)}
style={{
transform: `translateX(${100 * props.playhead}%)`
}}
/>
</div>
) : null}
{props.trimEnd === null ? null : (
+12 -2
View File
@@ -57,7 +57,7 @@
width: 100%;
height: 100%;
opacity: 0.75;
background-color: #8cbcff;
background-color: $drop-highlight;
transition: all 0.25s ease;
}
@@ -69,8 +69,18 @@
}
.backpack-item {
min-width: 4rem;
width: 4rem;
height: 4.5rem;
margin: 0 0.25rem;
flex-shrink: 0;
/* Need to hide overflow because of background setting below */
overflow: hidden;
}
.backpack-item > div {
/* Need to set the background to get blend-mode below to work */
background: $ui-primary;
}
.backpack-item img {
+1 -1
View File
@@ -13,7 +13,7 @@
width: 100%;
height: 100%;
opacity: 0.75;
background-color: #8cbcff;
background-color: $drop-highlight;
transition: all 0.25s ease;
}
+10 -4
View File
@@ -27,17 +27,23 @@
}
.illustration {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 208px;
background-color: $motion-primary;
background-image: url('./unsupported.png');
background-size: cover;
height: 100px;
background-color: $control-primary;
}
[dir="rtl"] .illustration {
transform: scaleX(-1);
}
.illustration img {
height: 80%;
width: auto;
}
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
+84 -62
View File
@@ -5,85 +5,107 @@ import Box from '../box/box.jsx';
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
import styles from './browser-modal.css';
import unhappyBrowser from './unsupported-browser.svg';
const messages = defineMessages({
label: {
id: 'gui.unsupportedBrowser.label',
defaultMessage: 'Browser is not supported',
description: ''
},
error: {
id: 'gui.unsupportedBrowser.errorLabel',
defaultMessage: 'An Error Occurred',
description: 'Heading shown when there is an unhandled exception in an unsupported browser'
}
});
const BrowserModal = ({intl, ...props}) => (
<ReactModal
isOpen
className={styles.modalContent}
contentLabel={intl.formatMessage({...messages.label})}
overlayClassName={styles.modalOverlay}
onRequestClose={props.onBack}
>
<div dir={props.isRtl ? 'rtl' : 'ltr'} >
<Box className={styles.illustration} />
<Box className={styles.body}>
<h2>
<FormattedMessage {...messages.label} />
</h2>
<p>
{ /* eslint-disable max-len */ }
<FormattedMessage
defaultMessage="We're very sorry, but Scratch 3.0 does not support Internet Explorer, Vivaldi, Opera or Silk. We recommend trying a newer browser such as Google Chrome, Mozilla Firefox, or Microsoft Edge."
description="Unsupported browser description"
id="gui.unsupportedBrowser.description"
/>
{ /* eslint-enable max-len */ }
</p>
<Box className={styles.buttonRow}>
<button
className={styles.backButton}
onClick={props.onBack}
>
<FormattedMessage
defaultMessage="Back"
description="Button to go back in unsupported browser modal"
id="gui.unsupportedBrowser.back"
/>
</button>
const BrowserModal = ({intl, ...props}) => {
const label = props.error ? messages.error : messages.label;
return (
<ReactModal
isOpen
className={styles.modalContent}
contentLabel={intl.formatMessage({...messages.label})}
overlayClassName={styles.modalOverlay}
onRequestClose={props.onBack}
>
<div dir={props.isRtl ? 'rtl' : 'ltr'} >
<Box className={styles.illustration}>
<img src={unhappyBrowser} />
</Box>
<div className={styles.faqLinkText}>
<FormattedMessage
defaultMessage="To learn more, go to the {previewFaqLink}."
description="Invitation to try 3.0 preview"
id="gui.unsupportedBrowser.previewfaq"
values={{
previewFaqLink: (
<a
className={styles.faqLink}
href="//scratch.mit.edu/3faq"
>
<FormattedMessage
defaultMessage="FAQ"
description="link to Scratch 3.0 FAQ page"
id="gui.unsupportedBrowser.previewfaqlinktext"
/>
</a>
)
}}
/>
</div>
</Box>
</div>
</ReactModal>
);
<Box className={styles.body}>
<h2>
<FormattedMessage {...label} />
</h2>
<p>
{ /* eslint-disable max-len */ }
{
props.error ? <FormattedMessage
defaultMessage="We are very sorry, but it looks like you are using a browser version that Scratch does not support. We recommend updating to the latest version of a supported browser such as Google Chrome, Mozilla Firefox, Microsoft Edge, or Apple Safari. "
description="Error message when the browser does not meet our minimum requirements"
id="gui.unsupportedBrowser.notRecommended"
/> : <FormattedMessage
defaultMessage="We are very sorry, but Scratch does not support this browser. We recommend updating to the latest version of a supported browser such as Google Chrome, Mozilla Firefox, Microsoft Edge, or Apple Safari."
description="Error message when the browser does not work at all (IE)"
id="gui.unsupportedBrowser.description"
/>
}
{ /* eslint-enable max-len */ }
</p>
<Box className={styles.buttonRow}>
<button
className={styles.backButton}
onClick={props.onBack}
>
<FormattedMessage
defaultMessage="Back"
description="Button to go back in unsupported browser modal"
id="gui.unsupportedBrowser.back"
/>
</button>
</Box>
<div className={styles.faqLinkText}>
<FormattedMessage
defaultMessage="To learn more, go to the {previewFaqLink}."
description="Invitation to try 3.0 preview"
id="gui.unsupportedBrowser.previewfaq"
values={{
previewFaqLink: (
<a
className={styles.faqLink}
href="//scratch.mit.edu/3faq"
>
<FormattedMessage
defaultMessage="FAQ"
description="link to Scratch 3.0 FAQ page"
id="gui.unsupportedBrowser.previewfaqlinktext"
/>
</a>
)
}}
/>
</div>
</Box>
</div>
</ReactModal>
);
};
BrowserModal.propTypes = {
error: PropTypes.bool,
intl: intlShape.isRequired,
isRtl: PropTypes.bool,
onBack: PropTypes.func.isRequired
};
BrowserModal.defaultProps = {
error: false
};
const WrappedBrowserModal = injectIntl(BrowserModal);
WrappedBrowserModal.setAppElement = ReactModal.setAppElement;
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="200px"
height="150px" viewBox="0 0 200 150" style="enable-background:new 0 0 200 150;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.1;fill:#231F20;stroke:#231F20;stroke-width:12;stroke-miterlimit:10;}
.st1{fill:#FFFFFF;stroke:#7F8CA5;stroke-width:2;stroke-miterlimit:10;}
.st2{fill:#BFC6D4;stroke:#7F8CA5;stroke-width:2;stroke-miterlimit:10;}
.st3{fill:#FFFFFF;}
.st4{opacity:0.25;}
.st5{fill:#231F20;}
.st6{opacity:0.15;}
.st7{fill:none;stroke:#231F20;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st8{fill:#7F9BD4;}
</style>
<g id="Layer_1">
</g>
<g id="Unsupported_Mask">
<g>
<g>
<path class="st0" d="M186,140H14c-2.21,0-4-1.79-4-4V14c0-2.21,1.79-4,4-4h172c2.21,0,4,1.79,4,4v122
C190,138.21,188.21,140,186,140z"/>
<path class="st1" d="M186,140H14c-2.21,0-4-1.79-4-4V14c0-2.21,1.79-4,4-4h172c2.21,0,4,1.79,4,4v122
C190,138.21,188.21,140,186,140z"/>
<path class="st2" d="M190,30H10V14c0-2.21,1.79-4,4-4h172c2.21,0,4,1.79,4,4V30z"/>
<path class="st3" d="M179.5,24h-128c-2.21,0-4-1.79-4-4v0c0-2.21,1.79-4,4-4h128c2.21,0,4,1.79,4,4v0
C183.5,22.21,181.71,24,179.5,24z"/>
<g class="st4">
<path class="st5" d="M24.09,20.22c-0.08,0.38-0.38,0.65-0.72,0.72l-2.87,0.66v1.77c0,0.44-0.55,0.66-0.87,0.36l-3.36-3.38
c-0.21-0.19-0.21-0.51,0-0.7l3.36-3.38c0.32-0.32,0.87-0.09,0.87,0.36v1.8l2.87,0.65C23.88,19.2,24.2,19.71,24.09,20.22z"/>
</g>
<g class="st4">
<path class="st5" d="M30.62,19.78c0.08-0.38,0.38-0.65,0.72-0.72l2.87-0.66v-1.77c0-0.44,0.55-0.66,0.87-0.36l3.36,3.38
c0.21,0.19,0.21,0.51,0,0.7l-3.36,3.38c-0.32,0.32-0.87,0.09-0.87-0.36v-1.8l-2.87-0.65C30.83,20.8,30.5,20.29,30.62,19.78z"/>
</g>
<g class="st6">
<line class="st7" x1="69.89" y1="20" x2="51.43" y2="20"/>
<line class="st7" x1="113.74" y1="20" x2="98.4" y2="20"/>
<line class="st7" x1="93.9" y1="20" x2="74.02" y2="20"/>
</g>
</g>
<g>
<circle class="st8" cx="89.61" cy="73.46" r="3.85"/>
<circle class="st8" cx="110.39" cy="73.46" r="3.85"/>
<g>
<path class="st8" d="M83.06,94.84c1.02-3.39,3.54-6.3,6.6-8.19c3.07-1.94,6.72-2.9,10.34-2.91c3.61,0.01,7.27,0.97,10.33,2.91
c3.06,1.89,5.58,4.8,6.6,8.19c0.16,0.53-0.14,1.1-0.68,1.26c-0.4,0.12-0.83-0.02-1.08-0.33l-0.02-0.03
c-3.85-4.75-9.5-7-15.17-7.01c-5.67,0.01-11.32,2.25-15.16,7.01l-0.02,0.02c-0.35,0.43-0.99,0.5-1.42,0.15
C83.06,95.66,82.95,95.23,83.06,94.84z"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+3 -1
View File
@@ -1,6 +1,8 @@
@import "../../css/units.css";
.outlined-button {
cursor: pointer;
border-radius: .25rem;
border-radius: $form-radius;
font-weight: bold;
display: flex;
flex-direction: row;
+5 -1
View File
@@ -234,7 +234,11 @@ const Cards = props => {
if (x === 0 && y === 0) {
// initialize positions
x = isRtl ? -292 : 292;
y = 365;
// The tallest cards are about 385px high, and the default position is pinned
// to near the bottom of the blocks palette to allow room to work above.
const tallCardHeight = 385;
const bottomMargin = 60; // To avoid overlapping the backpack region
y = window.innerHeight - tallCardHeight - bottomMargin;
}
const steps = content[activeDeckId].steps;
+2 -2
View File
@@ -28,8 +28,8 @@
.small {
width: 0.825rem;
height: 0.825rem;
color: $ui-white;
background-color: $motion-primary;
color: $ui-white;
}
.large {
@@ -38,7 +38,7 @@
box-shadow: 0 0 0 2px $ui-black-transparent;
}
.large.orange {
.large.orange {
background-color: hsla(29, 100%, 54%, 0.2);
box-shadow: 0px 0px 0px 2px hsla(29, 100%, 54%, 0.2);
}
+15 -3
View File
@@ -7,6 +7,8 @@ import closeIcon from './icon--close.svg';
import closeIconOrange from './icon--close-orange.svg';
import backIcon from '../../lib/assets/icon--back.svg';
let closeIcons = {};
const CloseButton = props => (
<div
aria-label="Close"
@@ -32,11 +34,11 @@ const CloseButton = props => (
className={classNames(
styles.closeIcon,
{
[styles.orange]: props.color === CloseButton.COLOR_ORANGE
[styles[props.color]]: (props.color !== CloseButton.COLOR_NEUTRAL)
}
)}
src={props.color === CloseButton.COLOR_ORANGE ?
closeIconOrange :
src={(props.color && closeIcons[props.color]) ?
closeIcons[props.color] :
closeIcon
}
/>
@@ -46,7 +48,16 @@ const CloseButton = props => (
CloseButton.SIZE_SMALL = 'small';
CloseButton.SIZE_LARGE = 'large';
CloseButton.COLOR_NEUTRAL = 'neutral';
CloseButton.COLOR_GREEN = 'green';
CloseButton.COLOR_ORANGE = 'orange';
closeIcons = {
[CloseButton.COLOR_NEUTRAL]: closeIcon,
[CloseButton.COLOR_GREEN]: closeIcon, // TODO: temporary, need green icon
[CloseButton.COLOR_ORANGE]: closeIconOrange
};
CloseButton.propTypes = {
buttonType: PropTypes.oneOf(['back', 'close']),
@@ -57,6 +68,7 @@ CloseButton.propTypes = {
};
CloseButton.defaultProps = {
color: CloseButton.COLOR_NEUTRAL,
size: CloseButton.SIZE_LARGE,
buttonType: 'close'
};
+3 -2
View File
@@ -5,12 +5,13 @@
*/
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/z-index.css";
.coming-soon {
background-color: $data-primary !important;
border: 1px solid $ui-black-transparent !important;
border-radius: .25rem !important;
border-radius: $form-radius !important;
box-shadow: 0 0 .5rem $ui-black-transparent !important;
padding: .75rem 1rem !important;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
@@ -25,7 +26,7 @@
border-left: 0 !important;
border-bottom: 0 !important;
border-right: 1px solid $ui-black-transparent !important;
border-radius: .25rem;
border-radius: $form-radius;
background-color: $data-primary !important;
height: 1rem !important;
width: 1rem !important;
@@ -63,7 +63,7 @@ const AutoScanningStep = props => (
</div>
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
{props.phase === PHASES.prescan && (
<FormattedMessage
defaultMessage="Have your device nearby, then begin searching."
@@ -80,60 +80,63 @@ const AutoScanningStep = props => (
)}
</Box>
<Dots
className={styles.bottomAreaItem}
counter={0}
total={3}
/>
{props.phase === PHASES.prescan && (
<button
className={styles.connectionButton}
onClick={props.onStartScan}
>
<FormattedMessage
defaultMessage="Start Searching"
description="Button in prompt for starting a search"
id="gui.connection.auto-scanning.start-search"
/>
</button>
)}
{props.phase === PHASES.pressbutton && (
<div className={styles.segmentedButton}>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
{props.phase === PHASES.prescan && (
<button
disabled
className={styles.connectionButton}
onClick={props.onStartScan}
>
<FormattedMessage
defaultMessage="Searching..."
description="Label indicating that search is in progress"
id="gui.connection.connecting-searchbutton"
defaultMessage="Start Searching"
description="Button in prompt for starting a search"
id="gui.connection.auto-scanning.start-search"
/>
</button>
)}
{props.phase === PHASES.pressbutton && (
<div className={styles.segmentedButton}>
<button
disabled
className={styles.connectionButton}
>
<FormattedMessage
defaultMessage="Searching..."
description="Label indicating that search is in progress"
id="gui.connection.connecting-searchbutton"
/>
</button>
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.abortConnectingIcon}
src={closeIcon}
/>
</button>
</div>
)}
{props.phase === PHASES.notfound && (
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.abortConnectingIcon}
src={closeIcon}
className={styles.buttonIconLeft}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Try again"
description="Button in prompt for trying a device search again"
id="gui.connection.auto-scanning.try-again"
/>
</button>
</div>
)}
{props.phase === PHASES.notfound && (
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.buttonIconLeft}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Try again"
description="Button in prompt for trying a device search again"
id="gui.connection.auto-scanning.try-again"
/>
</button>
)}
)}
</Box>
</Box>
</Box>
);
@@ -25,7 +25,7 @@ const ConnectedStep = props => (
</Box>
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Connected"
description="Message indicating that a device was connected"
@@ -34,9 +34,10 @@ const ConnectedStep = props => (
</Box>
<Dots
success
className={styles.bottomAreaItem}
total={3}
/>
<div className={styles.cornerButtons}>
<div className={classNames(styles.bottomAreaItem, styles.cornerButtons)}>
<button
className={classNames(styles.redButton, styles.connectionButton)}
onClick={props.onDisconnect}
@@ -1,6 +1,7 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import Dots from './dots.jsx';
@@ -27,14 +28,15 @@ const ConnectingStep = props => (
</Box>
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
{props.connectingMessage}
</Box>
<Dots
className={styles.bottomAreaItem}
counter={1}
total={3}
/>
<div className={styles.segmentedButton}>
<div className={classNames(styles.bottomAreaItem, styles.segmentedButton)}>
<button
disabled
className={styles.connectionButton}
@@ -29,7 +29,6 @@
overflow-y: auto;
width: 100%;
height: 100%;
padding: 0.5rem;
}
.peripheral-tile {
@@ -225,6 +224,7 @@
display: flex;
justify-content: center;
align-items: center;
padding: .5rem;
}
.scratch-link-help {
@@ -232,7 +232,8 @@
flex-direction: column;
justify-content: space-around;
height: 100%;
padding: 1rem 0;
padding-top: .5rem;
padding-bottom: .5rem;
}
.scratch-link-help-step {
@@ -243,11 +244,11 @@
}
[dir="ltr"] .scratch-link-help-step {
margin-left: 3rem;
margin-left: 2.5rem;
}
[dir="rtl"] .scratch-link-help-step {
margin-right: 3rem;
margin-right: 2.5rem;
}
.scratch-link-icon {
@@ -364,12 +365,18 @@
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 12px;
padding-top: 1rem;
padding-bottom: .75rem;
padding-left: .75rem;
padding-right: .75rem;
}
.bottom-area .bottom-area-item+.bottom-area-item {
margin-top: 1rem;
}
.instructions {
text-align: center;
margin-top: 1rem;
}
.dots-row {
@@ -377,7 +384,6 @@
flex-direction: row;
justify-content: center;
align-items: center;
margin: 1rem;
}
.dots-holder {
+7 -1
View File
@@ -6,7 +6,12 @@ import Box from '../box/box.jsx';
import styles from './connection-modal.css';
const Dots = props => (
<Box className={styles.dotsRow}>
<Box
className={classNames(
props.className,
styles.dotsRow
)}
>
<div
className={classNames(
styles.dotsHolder,
@@ -32,6 +37,7 @@ const Dots = props => (
);
Dots.propTypes = {
className: PropTypes.string,
counter: PropTypes.number,
error: PropTypes.bool,
success: PropTypes.bool,
@@ -23,7 +23,7 @@ const ErrorStep = props => (
</Box>
</Box>
<Box className={styles.bottomArea}>
<div className={styles.instructions}>
<div className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Oops, looks like something went wrong."
description="The device connection process has encountered an error."
@@ -32,9 +32,10 @@ const ErrorStep = props => (
</div>
<Dots
error
className={styles.bottomAreaItem}
total={3}
/>
<Box className={styles.buttonRow}>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
<button
className={styles.connectionButton}
onClick={props.onScanning}
@@ -55,7 +55,7 @@ const ScanningStep = props => (
)}
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Select your device in the list above."
description="Prompt for choosing a device to connect to"
@@ -63,11 +63,12 @@ const ScanningStep = props => (
/>
</Box>
<Dots
className={styles.bottomAreaItem}
counter={0}
total={3}
/>
<button
className={styles.connectionButton}
className={classNames(styles.bottomAreaItem, styles.connectionButton)}
onClick={props.onRefresh}
>
<FormattedMessage
@@ -57,9 +57,10 @@ const UnavailableStep = props => (
<Box className={styles.bottomArea}>
<Dots
error
className={styles.bottomAreaItem}
total={3}
/>
<Box className={styles.buttonRow}>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
<button
className={styles.connectionButton}
onClick={props.onScanning}
@@ -28,3 +28,11 @@
background: $motion-primary;
color: white;
}
.menu-item-bordered {
border-top: 1px solid $ui-black-transparent;
}
.menu-item-bordered:hover {
background: $error-primary;
}
@@ -1,5 +1,6 @@
import React from 'react';
import {ContextMenu, MenuItem} from 'react-contextmenu';
import classNames from 'classnames';
import styles from './context-menu.css';
@@ -17,7 +18,16 @@ const StyledMenuItem = props => (
/>
);
const BorderedMenuItem = props => (
<MenuItem
{...props}
attributes={{className: classNames(styles.menuItem, styles.menuItemBordered)}}
/>
);
export {
BorderedMenuItem,
StyledContextMenu as ContextMenu,
StyledMenuItem as MenuItem
};
@@ -29,6 +29,18 @@ const CrashMessage = props => (
id="gui.crashMessage.description"
/>
</p>
{props.eventId && (
<p>
<FormattedMessage
defaultMessage="Your error was logged with id {errorId}"
description="Message to inform the user that page has crashed."
id="gui.crashMessage.errorNumber"
values={{
errorId: props.eventId
}}
/>
</p>
)}
<button
className={styles.reloadButton}
onClick={props.onReload}
@@ -44,6 +56,7 @@ const CrashMessage = props => (
);
CrashMessage.propTypes = {
eventId: PropTypes.string,
onReload: PropTypes.func.isRequired
};
@@ -11,6 +11,11 @@
padding: 1.5rem 2.25rem;
}
.body input[type="checkbox"],
.body input[type="radio"] {
margin: 3px;
}
/* Blocks workspace for custom procedure declaration editor */
.workspace {
min-height: 200px;
+1 -2
View File
@@ -8,11 +8,10 @@
user-select: none;
user-drag: none;
cursor: pointer;
transition: 0.2s ease-out;
}
.green-flag:hover {
transform: scale(1.2);
background-color: $motion-light-transparent;
}
.green-flag.is-active {
+3 -4
View File
@@ -303,11 +303,10 @@ $fade-out-distance: 15px;
/* Alerts */
.alerts-container {
width: 520px;
display: flex;
justify-content: center;
width: 100%;
z-index: $z-index-alerts;
left: 0;
right: 0;
margin: auto;
position: absolute;
margin-top: 53px;
}
+61 -12
View File
@@ -31,6 +31,7 @@ import Cards from '../../containers/cards.jsx';
import Alerts from '../../containers/alerts.jsx';
import DragLayer from '../../containers/drag-layer.jsx';
import ConnectionModal from '../../containers/connection-modal.jsx';
import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx';
import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants';
import {resolveStageSize} from '../../lib/screen-utils';
@@ -58,16 +59,22 @@ const GUIComponent = props => {
accountNavOpen,
activeTabIndex,
alertsVisible,
authorId,
authorThumbnailUrl,
authorUsername,
basePath,
backdropLibraryVisible,
backpackOptions,
backpackHost,
backpackVisible,
blocksTabVisible,
cardsVisible,
canCreateNew,
canEditTitle,
canRemix,
canSave,
canCreateCopy,
canShare,
canUseCloud,
children,
connectionModalVisible,
costumeLibraryVisible,
@@ -75,6 +82,8 @@ const GUIComponent = props => {
enableCommunity,
importInfoVisible,
intl,
isCreating,
isFullScreen,
isPlayerOnly,
isRtl,
isShared,
@@ -89,16 +98,22 @@ const GUIComponent = props => {
onActivateCostumesTab,
onActivateSoundsTab,
onActivateTab,
onClickLogo,
onExtensionButtonClick,
onRequestCloseBackdropLibrary,
onRequestCloseCostumeLibrary,
onRequestCloseTelemetryModal,
onSeeCommunity,
onShare,
onTelemetryModalCancel,
onTelemetryModalOptIn,
onTelemetryModalOptOut,
previewInfoVisible,
showComingSoon,
soundsTabVisible,
stageSizeMode,
targetIsStage,
telemetryModalVisible,
tipsLibraryVisible,
vm,
...componentProps
@@ -125,8 +140,11 @@ const GUIComponent = props => {
return isPlayerOnly ? (
<StageWrapper
isFullScreen={isFullScreen}
isRendererSupported={isRendererSupported}
stageSize={stageSize}
isRtl={isRtl}
loading={loading}
stageSize={STAGE_SIZE_MODES.large}
vm={vm}
>
{alertsVisible ? (
@@ -142,9 +160,20 @@ const GUIComponent = props => {
{previewInfoVisible ? (
<PreviewModal />
) : null}
{telemetryModalVisible ? (
<TelemetryModal
onCancel={onTelemetryModalCancel}
onOptIn={onTelemetryModalOptIn}
onOptOut={onTelemetryModalOptOut}
onRequestClose={onRequestCloseTelemetryModal}
/>
) : null}
{loading ? (
<Loader />
) : null}
{isCreating ? (
<Loader messageId="gui.loader.creating" />
) : null}
{importInfoVisible ? (
<ImportModal />
) : null}
@@ -179,8 +208,12 @@ const GUIComponent = props => {
) : null}
<MenuBar
accountNavOpen={accountNavOpen}
authorId={authorId}
authorThumbnailUrl={authorThumbnailUrl}
authorUsername={authorUsername}
canCreateCopy={canCreateCopy}
canCreateNew={canCreateNew}
canEditTitle={canEditTitle}
canRemix={canRemix}
canSave={canSave}
canShare={canShare}
@@ -190,6 +223,7 @@ const GUIComponent = props => {
renderLogin={renderLogin}
showComingSoon={showComingSoon}
onClickAccountNav={onClickAccountNav}
onClickLogo={onClickLogo}
onCloseAccountNav={onCloseAccountNav}
onLogOut={onLogOut}
onOpenRegistration={onOpenRegistration}
@@ -261,6 +295,7 @@ const GUIComponent = props => {
<TabPanel className={tabClassNames.tabPanel}>
<Box className={styles.blocksWrapper}>
<Blocks
canUseCloud={canUseCloud}
grow={1}
isVisible={blocksTabVisible}
options={{
@@ -294,14 +329,15 @@ const GUIComponent = props => {
{soundsTabVisible ? <SoundTab vm={vm} /> : null}
</TabPanel>
</Tabs>
{backpackOptions.visible ? (
<Backpack host={backpackOptions.host} />
{backpackVisible ? (
<Backpack host={backpackHost} />
) : null}
</Box>
<Box className={classNames(styles.stageAndTargetWrapper, styles[stageSize])}>
<StageWrapper
isRendererSupported={isRendererSupported}
isRtl={isRtl}
stageSize={stageSize}
vm={vm}
/>
@@ -323,18 +359,21 @@ const GUIComponent = props => {
GUIComponent.propTypes = {
accountNavOpen: PropTypes.bool,
activeTabIndex: PropTypes.number,
authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // can be false
authorThumbnailUrl: PropTypes.string,
authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // can be false
backdropLibraryVisible: PropTypes.bool,
backpackOptions: PropTypes.shape({
host: PropTypes.string,
visible: PropTypes.bool
}),
backpackHost: PropTypes.string,
backpackVisible: PropTypes.bool,
basePath: PropTypes.string,
blocksTabVisible: PropTypes.bool,
canCreateCopy: PropTypes.bool,
canCreateNew: PropTypes.bool,
canEditTitle: PropTypes.bool,
canRemix: PropTypes.bool,
canSave: PropTypes.bool,
canShare: PropTypes.bool,
canUseCloud: PropTypes.bool,
cardsVisible: PropTypes.bool,
children: PropTypes.node,
costumeLibraryVisible: PropTypes.bool,
@@ -342,6 +381,8 @@ GUIComponent.propTypes = {
enableCommunity: PropTypes.bool,
importInfoVisible: PropTypes.bool,
intl: intlShape.isRequired,
isCreating: PropTypes.bool,
isFullScreen: PropTypes.bool,
isPlayerOnly: PropTypes.bool,
isRtl: PropTypes.bool,
isShared: PropTypes.bool,
@@ -350,15 +391,20 @@ GUIComponent.propTypes = {
onActivateSoundsTab: PropTypes.func,
onActivateTab: PropTypes.func,
onClickAccountNav: PropTypes.func,
onClickLogo: PropTypes.func,
onCloseAccountNav: PropTypes.func,
onExtensionButtonClick: PropTypes.func,
onLogOut: PropTypes.func,
onOpenRegistration: PropTypes.func,
onRequestCloseBackdropLibrary: PropTypes.func,
onRequestCloseCostumeLibrary: PropTypes.func,
onRequestCloseTelemetryModal: PropTypes.func,
onSeeCommunity: PropTypes.func,
onShare: PropTypes.func,
onTabSelect: PropTypes.func,
onTelemetryModalCancel: PropTypes.func,
onTelemetryModalOptIn: PropTypes.func,
onTelemetryModalOptOut: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
previewInfoVisible: PropTypes.bool,
@@ -367,22 +413,25 @@ GUIComponent.propTypes = {
soundsTabVisible: PropTypes.bool,
stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)),
targetIsStage: PropTypes.bool,
telemetryModalVisible: PropTypes.bool,
tipsLibraryVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
GUIComponent.defaultProps = {
backpackOptions: {
host: null,
visible: false
},
backpackHost: null,
backpackVisible: false,
basePath: './',
canCreateNew: false,
canEditTitle: false,
canRemix: false,
canSave: false,
canCreateCopy: false,
canShare: false,
canUseCloud: false,
enableCommunity: false,
isCreating: false,
isShared: false,
loading: false,
onUpdateProjectTitle: () => {},
showComingSoon: false,
stageSizeMode: STAGE_SIZE_MODES.large
@@ -5,16 +5,11 @@
flex-direction: column;
align-items: center;
cursor: pointer;
transition: 0.2s;
font-size: 0.75rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: $motion-primary;
}
.container:hover {
transform: scale(1.1);
}
.container + .container {
margin-top: 1.25rem;
}
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>Extensions/Connection/Bluetooth</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M10.0067929,12.1067505 L12.3957387,14.0016608 L10.0067929,15.9178959 L10.0067929,12.1067505 Z M10.0067929,4.08928413 L12.3854344,5.99452362 L10.0067929,7.87910469 L10.0067929,4.08928413 Z M8.88494896,1.00684787 C8.38336008,1.07015593 8.00841492,1.49998436 8.01240369,2.00678206 L8.01240369,7.96240477 L6.64126113,6.8795037 C6.43185026,6.70790553 6.16194292,6.62927025 5.89336518,6.66092428 C5.34557295,6.71856794 4.9480247,7.21003843 5.00552959,7.75915257 C5.03411584,8.03404284 5.17505267,8.28494269 5.39476788,8.45187606 L7.3476073,9.99292759 L5.39476788,11.5443083 C4.96165303,11.8895039 4.88952262,12.5212517 5.23388715,12.9550785 C5.57791929,13.3895718 6.20814627,13.461543 6.64126113,13.1166807 L8.01240369,12.0234504 L8.01240369,18.0000647 C8.01240369,18.5521777 8.45848208,18.9999989 9.00926589,18.9999989 C9.23596146,19.0003321 9.45600907,18.9230297 9.63284491,18.7810863 L14.6188179,14.7826823 C15.0519327,14.4418184 15.1270547,13.8130694 14.7870114,13.3789093 C14.7378164,13.3162677 14.6813087,13.2596236 14.6188179,13.21031 L10.5675486,10.0032568 L14.6188179,6.78587441 C15.0489412,6.44134526 15.1190772,5.81192984 14.7753774,5.38076861 C14.7291741,5.32279175 14.6766552,5.2701461 14.6188179,5.22383126 L9.63284491,1.22542729 C9.42343405,1.05416232 9.15352671,0.975193838 8.88494896,1.00684787 Z" id="path-1"></path>
</defs>
<g id="Extensions/Connection/Bluetooth" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="bluetooth" fill="#4D97FF" xlink:href="#path-1"></use>
<g id="Color/1_Blue" mask="url(#mask-2)" fill="#575E75">
<g id="Color/Gray">
<g id="Color">
<rect x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>Extensions/Connection/Wifi</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M14.2588749,9.82321433 C14.7290964,10.2624212 14.7491059,10.9837611 14.3189032,11.4539826 C14.0998,11.6940957 13.7886534,11.824157 13.4785073,11.824157 C13.1983753,11.824157 12.9182434,11.7141051 12.6981397,11.5130104 C11.2274469,10.1433652 8.77729262,10.1233557 7.2855899,11.4539826 C6.80536367,11.8741805 6.08502433,11.8341617 5.65482166,11.3539354 C5.23462371,10.8837139 5.27564304,10.1533699 5.7548688,9.73317192 C6.91541551,8.70268647 8.40611777,8.13241782 9.9568483,8.13241782 C11.5576024,8.13241782 13.0883235,8.73270061 14.2588749,9.82321433 Z M12.0380287,14.4151776 C12.0380287,15.4766776 11.1676187,16.3460872 10.1071191,16.3460872 C9.03661479,16.3460872 8.16620475,15.4766776 8.16620475,14.4151776 C8.16620475,13.3446733 9.03661479,12.4752637 10.1071191,12.4752637 C11.1676187,12.4752637 12.0380287,13.3446733 12.0380287,14.4151776 Z M17.6500725,6.11146577 C18.101285,6.55067268 18.1212945,7.27201249 17.6800866,7.7322293 C17.4509787,7.97234241 17.1498368,8.08239426 16.8506959,8.08239426 C16.5595588,8.08239426 16.2704225,7.98234712 16.0493184,7.76124296 C14.4095459,6.18149877 12.2385232,5.30008354 9.95744858,5.30008354 C7.70638813,5.30008354 5.57538424,6.15148463 3.93461129,7.69221044 C3.47339402,8.12241311 2.74405043,8.10240368 2.3128473,7.64218688 C1.87263992,7.18197008 1.90365453,6.45162602 2.36387134,6.01041817 C4.42384176,4.0705043 7.1251143,3 9.95744858,3 C12.8378055,3 15.5690922,4.11052315 17.6500725,6.11146577 Z" id="path-1"></path>
</defs>
<g id="Extensions/Connection/Wifi" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="wifi" fill="#4C97FF" xlink:href="#path-1"></use>
<g id="Color/1_Blue" mask="url(#mask-2)" fill="#575E75">
<g id="Color/Gray">
<g id="Color">
<rect x="0" y="0" width="20" height="20"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+82 -1
View File
@@ -7,8 +7,8 @@
align-items: center;
justify-content: flex-start;
flex-basis: 160px;
max-width: 160px;
height: 160px;
max-width: 160px;
margin: $space;
padding: 1rem 1rem 0 1rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
@@ -22,11 +22,19 @@
cursor: pointer;
}
.library-item-extension {
align-self: stretch;
}
.library-item:hover {
border-width: 2px;
border-color: $motion-primary;
}
.hidden {
display: none;
}
.disabled {
opacity: .5;
cursor: auto;
@@ -48,11 +56,37 @@
width: 100%;
}
.library-item-inset-image-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-top: -2.5rem;
align-self: flex-start;
background-color: $pen-primary;
width: 3.5rem;
height: 3.5rem;
border-radius: 0.5rem;
border: 0.25rem solid $ui-white;
}
[dir="ltr"] .library-item-inset-image-container {
margin-left: 1rem;
}
[dir="rtl"] .library-item-inset-image-container {
margin-right: 1rem;
}
.library-item-image {
max-width: 100%;
max-height: 100%;
}
.library-item-inset-image {
width: 2.5rem;
}
.library-item-name {
width: 80%;
margin: 0.25rem 0;
@@ -91,12 +125,59 @@
width: 300px;
}
[dir="ltr"] .featured-extension-text {
text-align: left;
padding-left: 1.25rem;
}
[dir="rtl"] .featured-extension-text {
text-align: right;
padding-right: 1.25rem;
}
.featured-description {
display: block;
font-weight: normal;
line-height: 2;
}
.featured-extension-metadata {
width: 100%;
padding: 0 1.25rem 1rem 1.25rem;
display: flex;
flex-direction: row;
align-items: flex-start;
font-size: 12px;
color: $text-primary;
opacity: 0.75;
}
[dir="ltr"] .featured-extension-metadata {
text-align: left;
}
[dir="rtl"] .featured-extension-metadata {
text-align: right;
}
.featured-extension-requirement {
width: 100%;
display: flex;
flex-direction: column;
}
.featured-extension-collaboration {
width: 100%;
display: flex;
flex-direction: column;
}
.featured-extension-metadata-detail {
height: 100%;
padding-top: 0.4rem;
font-weight: bold;
}
.coming-soon-text {
position: absolute;
background-color: $data-primary;
+93 -57
View File
@@ -1,4 +1,3 @@
import bindAll from 'lodash.bindall';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
@@ -7,42 +6,11 @@ import Box from '../box/box.jsx';
import styles from './library-item.css';
import classNames from 'classnames';
class LibraryItem extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'handleBlur',
'handleClick',
'handleFocus',
'handleKeyPress',
'handleMouseEnter',
'handleMouseLeave'
]);
}
handleBlur () {
this.props.onBlur(this.props.id);
}
handleFocus () {
this.props.onFocus(this.props.id);
}
handleClick (e) {
if (!this.props.disabled) {
this.props.onSelect(this.props.id);
}
e.preventDefault();
}
handleKeyPress (e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this.props.onSelect(this.props.id);
}
}
handleMouseEnter () {
this.props.onMouseEnter(this.props.id);
}
handleMouseLeave () {
this.props.onMouseLeave(this.props.id);
}
import bluetoothIconURL from './bluetooth.svg';
import internetConnectionIconURL from './internet-connection.svg';
/* eslint-disable react/prefer-stateless-function */
class LibraryItemComponent extends React.PureComponent {
render () {
return this.props.featured ? (
<div
@@ -51,9 +19,11 @@ class LibraryItem extends React.PureComponent {
styles.featuredItem,
{
[styles.disabled]: this.props.disabled
}
},
this.props.extensionId ? styles.libraryItemExtension : null,
this.props.hidden ? styles.hidden : null
)}
onClick={this.handleClick}
onClick={this.props.onClick}
>
<div className={styles.featuredImageContainer}>
{this.props.disabled ? (
@@ -70,25 +40,83 @@ class LibraryItem extends React.PureComponent {
src={this.props.iconURL}
/>
</div>
{this.props.insetIconURL ? (
<div className={styles.libraryItemInsetImageContainer}>
<img
className={styles.libraryItemInsetImage}
src={this.props.insetIconURL}
/>
</div>
) : null}
<div
className={styles.featuredText}
className={this.props.extensionId ?
classNames(styles.featuredExtensionText, styles.featuredText) : styles.featuredText
}
>
<span className={styles.libraryItemName}>{this.props.name}</span>
<br />
<span className={styles.featuredDescription}>{this.props.description}</span>
</div>
{this.props.bluetoothRequired || this.props.internetConnectionRequired || this.props.collaborator ? (
<div className={styles.featuredExtensionMetadata}>
<div className={styles.featuredExtensionRequirement}>
{this.props.bluetoothRequired || this.props.internetConnectionRequired ? (
<div>
<div>
<FormattedMessage
defaultMessage="Requires"
description="Label for extension hardware requirements"
id="gui.extensionLibrary.requires"
/>
</div>
<div
className={styles.featuredExtensionMetadataDetail}
>
<img
src={this.props.bluetoothRequired ?
bluetoothIconURL :
internetConnectionIconURL
}
/>
</div>
</div>
) : null}
</div>
<div className={styles.featuredExtensionCollaboration}>
{this.props.collaborator ? (
<div>
<div>
<FormattedMessage
defaultMessage="Collaboration with"
description="Label for extension collaboration"
id="gui.extensionLibrary.collaboration"
/>
</div>
<div
className={styles.featuredExtensionMetadataDetail}
>
{this.props.collaborator}
</div>
</div>
) : null}
</div>
</div>
) : null}
</div>
) : (
<Box
className={styles.libraryItem}
className={classNames(
styles.libraryItem,
this.props.hidden ? styles.hidden : null
)}
role="button"
tabIndex="0"
onBlur={this.handleBlur}
onClick={this.handleClick}
onFocus={this.handleFocus}
onKeyPress={this.handleKeyPress}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onBlur={this.props.onBlur}
onClick={this.props.onClick}
onFocus={this.props.onFocus}
onKeyPress={this.props.onKeyPress}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
>
{/* Layers of wrapping is to prevent layout thrashing on animation */}
<Box className={styles.libraryItemImageContainerWrapper}>
@@ -104,29 +132,37 @@ class LibraryItem extends React.PureComponent {
);
}
}
/* eslint-enable react/prefer-stateless-function */
LibraryItem.propTypes = {
LibraryItemComponent.propTypes = {
bluetoothRequired: PropTypes.bool,
collaborator: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
disabled: PropTypes.bool,
extensionId: PropTypes.string,
featured: PropTypes.bool,
iconURL: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
hidden: PropTypes.bool,
iconURL: PropTypes.string,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
name: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]).isRequired,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
]),
onBlur: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired
onMouseLeave: PropTypes.func.isRequired
};
LibraryItem.defaultProps = {
LibraryItemComponent.defaultProps = {
disabled: false
};
export default LibraryItem;
export default LibraryItemComponent;
+1 -1
View File
@@ -10,7 +10,7 @@
flex-grow: 1;
flex-wrap: wrap;
overflow-y: auto;
height: calc(100% - $library-header-height);
height: auto;
padding: 0.5rem;
}
+29 -34
View File
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import LibraryItem from '../library-item/library-item.jsx';
import LibraryItem from '../../containers/library-item.jsx';
import Modal from '../../containers/modal.jsx';
import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
@@ -33,11 +33,9 @@ class LibraryComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleBlur',
'handleClose',
'handleFilterChange',
'handleFilterClear',
'handleFocus',
'handleMouseEnter',
'handleMouseLeave',
'handleSelect',
@@ -56,12 +54,6 @@ class LibraryComponent extends React.Component {
this.scrollToTop();
}
}
handleBlur (id) {
this.handleMouseLeave(id);
}
handleFocus (id) {
this.handleMouseEnter(id);
}
handleSelect (id) {
this.handleClose();
this.props.onItemSelected(this.getFilteredData()[id]);
@@ -98,10 +90,12 @@ class LibraryComponent extends React.Component {
(dataItem.tags || [])
// Second argument to map sets `this`
.map(String.prototype.toLowerCase.call, String.prototype.toLowerCase)
.concat((typeof dataItem.name === 'string' ?
.concat(dataItem.name ?
(typeof dataItem.name === 'string' ?
// Use the name if it is a string, else use formatMessage to get the translated name
dataItem.name : this.props.intl.formatMessage(dataItem.name.props)
).toLowerCase())
dataItem.name : this.props.intl.formatMessage(dataItem.name.props)
).toLowerCase() :
null)
.join('\n') // unlikely to partially match newlines
.indexOf(this.state.filterQuery.toLowerCase()) !== -1
));
@@ -170,27 +164,28 @@ class LibraryComponent extends React.Component {
})}
ref={this.setFilteredDataRef}
>
{this.getFilteredData().map((dataItem, index) => {
const scratchURL = dataItem.md5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` :
dataItem.rawURL;
return (
<LibraryItem
description={dataItem.description}
disabled={dataItem.disabled}
featured={dataItem.featured}
iconURL={scratchURL}
id={index}
key={`item_${index}`}
name={dataItem.name}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
);
})}
{this.getFilteredData().map((dataItem, index) => (
<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
disabled={dataItem.disabled}
extensionId={dataItem.extensionId}
featured={dataItem.featured}
hidden={dataItem.hidden}
iconMd5={dataItem.md5}
iconRawURL={dataItem.rawURL}
icons={dataItem.json && dataItem.json.costumes}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
key={`item_${index}`}
name={dataItem.name}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
))}
</div>
</Modal>
);
@@ -207,7 +202,7 @@ LibraryComponent.propTypes = {
name: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]).isRequired,
]),
rawURL: PropTypes.string
})
/* eslint-enable react/no-unused-prop-types, lines-around-comment */
+8 -1
View File
@@ -2,7 +2,7 @@
@import "../../css/z-index.css";
.background {
position: fixed;
position: absolute;
top: 0;
left: 0;
width: 100%;
@@ -17,6 +17,13 @@
color: white;
}
.fullscreen {
/* Break out of the layout using position: fixed to cover the whole screen */
position: fixed;
/* Use the fullscreen stage z-index to allow covering full-screen mode */
z-index: $z-index-stage-wrapper-overlay;
}
.block-animation {
width: 125px;
height: 150px;
+33 -6
View File
@@ -1,6 +1,8 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import styles from './loader.css';
import PropTypes from 'prop-types';
import topBlock from './top-block.svg';
import middleBlock from './middle-block.svg';
@@ -97,6 +99,22 @@ const messages = [
weight: 1
}
];
const mainMessages = {
'gui.loader.headline': (
<FormattedMessage
defaultMessage="Loading Project"
description="Main loading message"
id="gui.loader.headline"
/>
),
'gui.loader.creating': (
<FormattedMessage
defaultMessage="Creating Project"
description="Main creating message"
id="gui.loader.creating"
/>
)
};
class LoaderComponent extends React.Component {
constructor (props) {
@@ -131,7 +149,11 @@ class LoaderComponent extends React.Component {
}
render () {
return (
<div className={styles.background}>
<div
className={classNames(styles.background, {
[styles.fullscreen]: this.props.isFullScreen
})}
>
<div className={styles.container}>
<div className={styles.blockAnimation}>
<img
@@ -148,11 +170,7 @@ class LoaderComponent extends React.Component {
/>
</div>
<div className={styles.title}>
<FormattedMessage
defaultMessage="Loading Project"
description="Main loading message"
id="gui.loader.headline"
/>
{mainMessages[this.props.messageId]}
</div>
<div className={styles.messageContainerOuter}>
<div
@@ -175,4 +193,13 @@ class LoaderComponent extends React.Component {
}
}
LoaderComponent.propTypes = {
isFullScreen: PropTypes.bool,
messageId: PropTypes.string
};
LoaderComponent.defaultProps = {
isFullScreen: false,
messageId: 'gui.loader.headline'
};
export default LoaderComponent;
+7 -4
View File
@@ -28,11 +28,8 @@
}
.user-info .avatar {
margin-right: calc($space * .8125);
border-radius: $form-radius;
width: 2rem;
height: 2rem;
vertical-align: middle;
}
[dir="ltr"] .user-info .dropdown-caret-position {
@@ -45,6 +42,12 @@
.user-info .dropdown-caret-position {
display: inline-block;
padding-bottom: .125rem;
padding-bottom: .0625rem;
vertical-align: middle;
}
.user-info .profile-name {
font-size: .75rem;
line-height: .9375rem;
font-weight: bold;
}
+3 -2
View File
@@ -12,6 +12,7 @@ import React from 'react';
import MenuBarMenu from './menu-bar-menu.jsx';
import {MenuSection} from '../menu/menu.jsx';
import MenuItemContainer from '../../containers/menu-item.jsx';
import UserAvatar from './user-avatar.jsx';
import dropdownCaret from './dropdown-caret.svg';
import styles from './account-nav.css';
@@ -40,9 +41,9 @@ const AccountNavComponent = ({
onMouseUp={onClick}
>
{thumbnailUrl ? (
<img
<UserAvatar
className={styles.avatar}
src={thumbnailUrl}
imageUrl={thumbnailUrl}
/>
) : null}
<span className={styles.profileName}>
+39
View File
@@ -0,0 +1,39 @@
@import "../../css/colors.css";
@import "../../css/units.css";
.author-info {
color: $ui-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
display: flex;
justify-content: start;
align-items: center;
cursor: default;
}
.avatar {
margin-right: .5625rem;
}
.project-title {
max-width: $menu-bar-item-max-width;
display: block;
overflow: hidden;
font-size: $menu-bar-large-font-size;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
}
.username-line {
max-width: $menu-bar-item-max-width;
font-size: $menu-bar-standard-font-size;
display: block;
overflow: hidden;
font-weight: normal;
text-overflow: ellipsis;
white-space: nowrap;
}
.username {
font-weight: bold;
}
+55
View File
@@ -0,0 +1,55 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import {FormattedMessage, injectIntl} from 'react-intl';
import UserAvatar from './user-avatar.jsx';
import styles from './author-info.css';
const AuthorInfo = ({
className,
imageUrl,
projectTitle,
// TODO: use userId to link to user's profile
userId, // eslint-disable-line no-unused-vars
username
}) => (
<div
className={classNames(
className,
styles.authorInfo
)}
>
<UserAvatar
className={styles.avatar}
imageUrl={imageUrl}
/>
<div className={styles.titleAuthor}>
<span className={styles.projectTitle}>
{projectTitle}
</span>
<div>
<span className={styles.usernameLine}>
<FormattedMessage
defaultMessage="by {username}"
description="Shows that a project was created by this user"
id="gui.authorInfo.byUser"
values={{
username: <span className={styles.username}>{username}</span>
}}
/>
</span>
</div>
</div>
</div>
);
AuthorInfo.propTypes = {
className: PropTypes.string,
imageUrl: PropTypes.string,
projectTitle: PropTypes.string,
userId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
username: PropTypes.oneOfType([PropTypes.string, PropTypes.bool])
};
export default injectIntl(AuthorInfo);
@@ -0,0 +1,9 @@
@import "../../css/colors.css";
.community-button {
box-shadow: 0 0 0 1px $ui-black-transparent;
}
.community-button-icon {
height: 1.25rem;
}
@@ -0,0 +1,40 @@
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import Button from '../button/button.jsx';
import communityIcon from './icon--see-community.svg';
import styles from './community-button.css';
const CommunityButton = ({
className,
onClick
}) => (
<Button
className={classNames(
className,
styles.communityButton
)}
iconClassName={styles.communityButtonIcon}
iconSrc={communityIcon}
onClick={onClick}
>
<FormattedMessage
defaultMessage="See Project Page"
description="Label for see project page button"
id="gui.menuBar.seeProjectPage"
/>
</Button>
);
CommunityButton.propTypes = {
className: PropTypes.string,
onClick: PropTypes.func
};
CommunityButton.defaultProps = {
onClick: () => {}
};
export default CommunityButton;
@@ -35,6 +35,11 @@ const LoginDropdownMessages = defineMessages({ // eslint-disable-line no-unused-
defaultMessage: 'Need Help?',
description: 'Button text for user to indicate that they need help',
id: 'login.needHelp'
},
validationRequired: {
defaultMessage: 'This field is required',
description: 'Message to tell user they must enter text in a form field',
id: 'form.validationRequired'
}
});
+14 -34
View File
@@ -25,7 +25,7 @@
width: 100%;
*/
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: .75rem;
font-size: $menu-bar-standard-font-size;
font-weight: bold;
background-color: $motion-primary;
color: $ui-white;
@@ -45,6 +45,10 @@
vertical-align: middle;
}
.scratch-logo.clickable {
cursor: pointer;
}
.language-icon {
height: 1.5rem;
vertical-align: middle;
@@ -83,7 +87,7 @@
}
.menu-bar-item.growable {
max-width: 12rem;
max-width: $menu-bar-item-max-width;
flex: 1;
}
@@ -126,24 +130,13 @@
height: 34px;
}
.author-info {
margin-left: .25rem;
margin-right: .6875rem;
}
.menu-bar-button {
height: 2rem;
}
.share-button {
background: $data-primary;
}
.share-button-is-shared {
background: $ui-black-transparent;
}
.community-button {
box-shadow: 0 0 0 1px $ui-black-transparent;
}
.community-button-icon {
height: 1.25rem;
height: $menu-bar-button-size;
}
.remix-button {
@@ -186,8 +179,8 @@
.profile-icon {
margin: 0 .25rem;
width: 2rem;
border-radius: 0.25rem;
width: $menu-bar-button-size;
border-radius: $form-radius;
}
.dropdown-caret-icon {
@@ -207,19 +200,6 @@
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;
+156 -113
View File
@@ -7,13 +7,18 @@ import React from 'react';
import Box from '../box/box.jsx';
import Button from '../button/button.jsx';
import CommunityButton from './community-button.jsx';
import ShareButton from './share-button.jsx';
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import Divider from '../divider/divider.jsx';
import LanguageSelector from '../../containers/language-selector.jsx';
import SaveStatus from './save-status.jsx';
import SBFileUploader from '../../containers/sb-file-uploader.jsx';
import ProjectWatcher from '../../containers/project-watcher.jsx';
import MenuBarMenu from './menu-bar-menu.jsx';
import {MenuItem, MenuSection} from '../menu/menu.jsx';
import ProjectTitleInput from './project-title-input.jsx';
import AuthorInfo from './author-info.jsx';
import AccountNav from '../../containers/account-nav.jsx';
import LoginDropdown from './login-dropdown.jsx';
import SB3Downloader from '../../containers/sb3-downloader.jsx';
@@ -23,11 +28,12 @@ import TurboMode from '../../containers/turbo-mode.jsx';
import {openTipsLibrary} from '../../reducers/modals';
import {setPlayer} from '../../reducers/mode';
import {
autoUpdateProject,
getIsUpdating,
getIsShowingProject,
manualUpdateProject,
requestNewProject,
remixProject,
updateProject,
saveProjectAsCopy
} from '../../reducers/project-state';
import {
@@ -54,13 +60,19 @@ import helpIcon from '../../lib/assets/icon--tutorials.svg';
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 remixIcon from './icon--remix.svg';
import dropdownCaret from './dropdown-caret.svg';
import languageIcon from '../language-selector/language-icon.svg';
import scratchLogo from './scratch-logo.svg';
const messages = defineMessages({
confirmNav: {
id: 'gui.menuBar.confirmNewWithoutSaving',
defaultMessage: 'Replace contents of the current project?',
description: 'message for prompting user to confirm that they want to create new project without saving'
}
});
const ariaMessages = defineMessages({
language: {
id: 'gui.menuBar.LanguageSelector',
@@ -136,37 +148,64 @@ class MenuBar extends React.Component {
'handleClickRemix',
'handleClickSave',
'handleClickSaveAsCopy',
'handleClickSeeCommunity',
'handleClickShare',
'handleCloseFileMenuAndThen',
'handleLanguageMouseUp',
'handleRestoreOption',
'restoreOptionMessage'
]);
}
componentDidUpdate (prevProps) {
// if we're no longer showing the project (loading, or whatever), close menus
if (this.props.isShowingProject && !prevProps.isShowingProject) {
this.props.onRequestCloseFile();
this.props.onRequestCloseEdit();
}
}
handleClickNew () {
// if canSave===true and canCreateNew===true, it's safe to replace current project,
// since we will auto-save first. Else, confirm first.
const readyToReplaceProject = (this.props.canSave && this.props.canCreateNew) ||
confirm('Replace contents of the current project?'); // eslint-disable-line no-alert
let readyToReplaceProject = true;
// if the project is dirty, and user owns the project, we will autosave.
// but if they are not logged in and can't save, user should consider
// downloading or logging in first.
// Note that if user is logged in and editing someone else's project,
// they'll lose their work.
if (this.props.projectChanged && !this.props.canCreateNew) {
readyToReplaceProject = confirm( // eslint-disable-line no-alert
this.props.intl.formatMessage(messages.confirmNav)
);
}
this.props.onRequestCloseFile();
if (readyToReplaceProject) {
this.props.onClickNew(this.props.canSave && this.props.canCreateNew);
}
this.props.onRequestCloseFile();
}
handleClickRemix () {
this.props.onClickRemix();
this.props.onRequestCloseFile();
}
handleClickSave () {
this.props.onClickSave();
this.props.onRequestCloseFile();
}
handleClickSaveAsCopy () {
this.props.onClickSaveAsCopy();
this.props.onRequestCloseFile();
}
handleClickSeeCommunity (waitForUpdate) {
if (this.props.canSave) { // save before transitioning to project page
this.props.autoUpdateProject();
waitForUpdate(true); // queue the transition to project page
} else {
waitForUpdate(false); // immediately transition to project page
}
}
handleClickShare (waitForUpdate) {
if (!this.props.isShared) {
if (this.props.canShare) { // save before transitioning to project page
this.props.onShare();
}
if (this.props.canSave) { // save before transitioning to project page
this.props.autoUpdateProject();
waitForUpdate(true); // queue the transition to project page
} else {
waitForUpdate(false); // immediately transition to project page
}
}
}
handleRestoreOption (restoreFun) {
return () => {
@@ -243,32 +282,6 @@ class MenuBar extends React.Component {
id="gui.menuBar.new"
/>
);
const shareMessage = (
<FormattedMessage
defaultMessage="Share"
description="Label for project share button"
id="gui.menuBar.share"
/>
);
const isSharedMessage = (
<FormattedMessage
defaultMessage="Shared"
description="Label for shared project"
id="gui.menuBar.isShared"
/>
);
const shareButton = (
<Button
className={classNames(
styles.menuBarButton,
styles.shareButton,
{[styles.shareButtonIsShared]: this.props.isShared}
)}
onClick={this.props.onShare}
>
{this.props.isShared ? isSharedMessage : shareMessage}
</Button>
);
const remixButton = (
<Button
className={classNames(
@@ -286,25 +299,21 @@ class MenuBar extends React.Component {
<Box
className={classNames(
this.props.className,
styles.menuBar,
{[styles.saveInProgress]: this.props.isUpdating}
styles.menuBar
)}
>
<div className={styles.mainMenu}>
<div className={styles.fileGroup}>
<div className={classNames(styles.menuBarItem)}>
<a
href="https://scratch.mit.edu"
rel="noopener noreferrer"
target="_blank"
>
<img
alt="Scratch"
className={styles.scratchLogo}
draggable={false}
src={scratchLogo}
/>
</a>
<img
alt="Scratch"
className={classNames(styles.scratchLogo, {
[styles.clickable]: typeof this.props.onClickLogo !== 'undefined'
})}
draggable={false}
src={scratchLogo}
onClick={this.props.onClickLogo}
/>
</div>
<div
className={classNames(styles.menuBarItem, styles.hoverable, styles.languageMenu)}
@@ -346,23 +355,25 @@ class MenuBar extends React.Component {
{newProjectMessage}
</MenuItem>
</MenuSection>
<MenuSection>
{this.props.canSave ? (
<MenuItem onClick={this.handleClickSave}>
{saveNowMessage}
</MenuItem>
) : []}
{this.props.canCreateCopy ? (
<MenuItem onClick={this.handleClickSaveAsCopy}>
{createCopyMessage}
</MenuItem>
) : []}
{this.props.canRemix ? (
<MenuItem onClick={this.handleClickRemix}>
{remixMessage}
</MenuItem>
) : []}
</MenuSection>
{(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && (
<MenuSection>
{this.props.canSave ? (
<MenuItem onClick={this.handleClickSave}>
{saveNowMessage}
</MenuItem>
) : []}
{this.props.canCreateCopy ? (
<MenuItem onClick={this.handleClickSaveAsCopy}>
{createCopyMessage}
</MenuItem>
) : []}
{this.props.canRemix ? (
<MenuItem onClick={this.handleClickRemix}>
{remixMessage}
</MenuItem>
) : []}
</MenuSection>
)}
<MenuSection>
<SBFileUploader onUpdateProjectTitle={this.props.onUpdateProjectTitle}>
{(className, renderFileInput, loadProject) => (
@@ -458,22 +469,50 @@ class MenuBar extends React.Component {
<FormattedMessage {...ariaMessages.tutorials} />
</div>
<Divider className={classNames(styles.divider)} />
<div className={classNames(styles.menuBarItem, styles.growable)}>
<MenuBarItemTooltip
enable
id="title-field"
>
<ProjectTitleInput
className={classNames(styles.titleFieldGrowable)}
onUpdateProjectTitle={this.props.onUpdateProjectTitle}
/>
</MenuBarItemTooltip>
</div>
{this.props.canEditTitle ? (
<div className={classNames(styles.menuBarItem, styles.growable)}>
<MenuBarItemTooltip
enable
id="title-field"
>
<ProjectTitleInput
className={classNames(styles.titleFieldGrowable)}
onUpdateProjectTitle={this.props.onUpdateProjectTitle}
/>
</MenuBarItemTooltip>
</div>
) : ((this.props.authorUsername && this.props.authorUsername !== this.props.username) ? (
<AuthorInfo
className={styles.authorInfo}
imageUrl={this.props.authorThumbnailUrl}
projectTitle={this.props.projectTitle}
userId={this.props.authorId}
username={this.props.authorUsername}
/>
) : null)}
<div className={classNames(styles.menuBarItem)}>
{this.props.canShare ? shareButton : (
{this.props.canShare ? (
(this.props.isShowingProject || this.props.isUpdating) && (
<ProjectWatcher onDoneUpdating={this.props.onSeeCommunity}>
{
waitForUpdate => (
<ShareButton
className={styles.menuBarButton}
isShared={this.props.isShared}
/* eslint-disable react/jsx-no-bind */
onClick={() => {
this.handleClickShare(waitForUpdate);
}}
/* eslint-enable react/jsx-no-bind */
/>
)
}
</ProjectWatcher>
)
) : (
this.props.showComingSoon ? (
<MenuBarItemTooltip id="share-button">
{shareButton}
<ShareButton className={styles.menuBarButton} />
</MenuBarItemTooltip>
) : []
)}
@@ -481,37 +520,25 @@ class MenuBar extends React.Component {
</div>
<div className={classNames(styles.menuBarItem, styles.communityButtonWrapper)}>
{this.props.enableCommunity ? (
<Button
className={classNames(
styles.menuBarButton,
styles.communityButton
)}
iconClassName={styles.communityButtonIcon}
iconSrc={communityIcon}
onClick={this.props.onSeeCommunity}
>
<FormattedMessage
defaultMessage="See Community"
description="Label for see community button"
id="gui.menuBar.seeCommunity"
/>
</Button>
(this.props.isShowingProject || this.props.isUpdating) && (
<ProjectWatcher onDoneUpdating={this.props.onSeeCommunity}>
{
waitForUpdate => (
<CommunityButton
className={styles.menuBarButton}
/* eslint-disable react/jsx-no-bind */
onClick={() => {
this.handleClickSeeCommunity(waitForUpdate);
}}
/* eslint-enable react/jsx-no-bind */
/>
)
}
</ProjectWatcher>
)
) : (this.props.showComingSoon ? (
<MenuBarItemTooltip id="community-button">
<Button
className={classNames(
styles.menuBarButton,
styles.communityButton
)}
iconClassName={styles.communityButtonIcon}
iconSrc={communityIcon}
>
<FormattedMessage
defaultMessage="See Community"
description="Label for see community button"
id="gui.menuBar.seeCommunity"
/>
</Button>
<CommunityButton className={styles.menuBarButton} />
</MenuBarItemTooltip>
) : [])}
</div>
@@ -520,6 +547,11 @@ class MenuBar extends React.Component {
{/* 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 */}
<div className={styles.accountInfoGroup}>
<div className={styles.menuBarItem}>
{this.props.canSave && (
<SaveStatus />
)}
</div>
{this.props.sessionExists ? (
this.props.username ? (
// ************ user is logged in ************
@@ -667,8 +699,13 @@ class MenuBar extends React.Component {
MenuBar.propTypes = {
accountMenuOpen: PropTypes.bool,
authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
authorThumbnailUrl: PropTypes.string,
authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
autoUpdateProject: PropTypes.func,
canCreateCopy: PropTypes.bool,
canCreateNew: PropTypes.bool,
canEditTitle: PropTypes.bool,
canRemix: PropTypes.bool,
canSave: PropTypes.bool,
canShare: PropTypes.bool,
@@ -688,6 +725,7 @@ MenuBar.propTypes = {
onClickFile: PropTypes.func,
onClickLanguage: PropTypes.func,
onClickLogin: PropTypes.func,
onClickLogo: PropTypes.func,
onClickNew: PropTypes.func,
onClickRemix: PropTypes.func,
onClickSave: PropTypes.func,
@@ -704,6 +742,8 @@ MenuBar.propTypes = {
onShare: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectChanged: PropTypes.bool,
projectTitle: PropTypes.string,
renderLogin: PropTypes.func,
sessionExists: PropTypes.bool,
showComingSoon: PropTypes.bool,
@@ -726,12 +766,15 @@ const mapStateToProps = state => {
isShowingProject: getIsShowingProject(loadingState),
languageMenuOpen: languageMenuOpen(state),
loginMenuOpen: loginMenuOpen(state),
projectChanged: state.scratchGui.projectChanged,
projectTitle: state.scratchGui.projectTitle,
sessionExists: state.session && typeof state.session.session !== 'undefined',
username: user ? user.username : null
};
};
const mapDispatchToProps = dispatch => ({
autoUpdateProject: () => dispatch(autoUpdateProject()),
onOpenTipLibrary: () => dispatch(openTipsLibrary()),
onClickAccount: () => dispatch(openAccountMenu()),
onRequestCloseAccount: () => dispatch(closeAccountMenu()),
@@ -745,7 +788,7 @@ const mapDispatchToProps = dispatch => ({
onRequestCloseLogin: () => dispatch(closeLoginMenu()),
onClickNew: needSave => dispatch(requestNewProject(needSave)),
onClickRemix: () => dispatch(remixProject()),
onClickSave: () => dispatch(updateProject()),
onClickSave: () => dispatch(manualUpdateProject()),
onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()),
onSeeCommunity: () => dispatch(setPlayer(true))
});
+4
View File
@@ -0,0 +1,4 @@
.save-now {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
cursor: pointer;
}
+61
View File
@@ -0,0 +1,61 @@
import {connect} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import InlineMessages from '../../containers/inline-messages.jsx';
import {
manualUpdateProject
} from '../../reducers/project-state';
import {
filterInlineAlerts
} from '../../reducers/alerts';
import styles from './save-status.css';
// Wrapper for inline messages in the nav bar, which are all related to saving.
// Show any inline messages if present, else show the "Save Now" button if the
// project has changed.
// We decided to not use an inline message for "Save Now" because it is a reflection
// of the project state, rather than an event.
const SaveStatus = ({
alertsList,
projectChanged,
onClickSave
}) => (
filterInlineAlerts(alertsList).length > 0 ? (
<InlineMessages />
) : projectChanged && (
<div
className={styles.saveNow}
onClick={onClickSave}
>
<FormattedMessage
defaultMessage="Save Now"
description="Title bar link for saving now"
id="gui.menuBar.saveNowLink"
/>
</div>
));
SaveStatus.propTypes = {
alertsList: PropTypes.arrayOf(PropTypes.object),
onClickSave: PropTypes.func,
projectChanged: PropTypes.bool
};
const mapStateToProps = state => ({
alertsList: state.scratchGui.alerts.alertsList,
projectChanged: state.scratchGui.projectChanged
});
const mapDispatchToProps = dispatch => ({
onClickSave: () => dispatch(manualUpdateProject())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SaveStatus);
+10
View File
@@ -0,0 +1,10 @@
@import "../../css/colors.css";
.share-button {
background: $data-primary;
}
.share-button-is-shared {
background: $ui-black-transparent;
cursor: default;
}
+48
View File
@@ -0,0 +1,48 @@
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import Button from '../button/button.jsx';
import styles from './share-button.css';
const ShareButton = ({
className,
isShared,
onClick
}) => (
<Button
className={classNames(
className,
styles.shareButton,
{[styles.shareButtonIsShared]: isShared}
)}
onClick={onClick}
>
{isShared ? (
<FormattedMessage
defaultMessage="Shared"
description="Label for shared project"
id="gui.menuBar.isShared"
/>
) : (
<FormattedMessage
defaultMessage="Share"
description="Label for project share button"
id="gui.menuBar.share"
/>
)}
</Button>
);
ShareButton.propTypes = {
className: PropTypes.string,
isShared: PropTypes.bool,
onClick: PropTypes.func
};
ShareButton.defaultProps = {
onClick: () => {}
};
export default ShareButton;
+10
View File
@@ -0,0 +1,10 @@
@import "../../css/colors.css";
@import "../../css/units.css";
.user-thumbnail {
width: $menu-bar-button-size;
height: $menu-bar-button-size;
border-radius: $form-radius;
vertical-align: middle;
box-shadow: 0 0 0 1px $ui-black-transparent;
}
+25
View File
@@ -0,0 +1,25 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import styles from './user-avatar.css';
const UserAvatar = ({
className,
imageUrl
}) => (
<img
className={classNames(
className,
styles.userThumbnail
)}
src={imageUrl}
/>
);
UserAvatar.propTypes = {
className: PropTypes.string,
imageUrl: PropTypes.string
};
export default UserAvatar;
+18
View File
@@ -1,3 +1,5 @@
@import "../../css/colors.css";
.green {
fill: rgb(171, 220, 170);
stroke: rgb(174, 211, 168);
@@ -12,3 +14,19 @@
fill: rgb(251, 194, 142);
stroke: rgb(235, 189, 142);
}
.mask-container {
position: relative;
}
.mask {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
transform-origin: top;
will-change: transform;
background: $ui-primary;
opacity: 0.75;
}
+29 -25
View File
@@ -20,36 +20,40 @@ const Meter = props => {
const barHeight = (height - (barSpacing * (nBars + 1))) / nBars;
const nBarsToMask = nBars - Math.floor(level * nBars);
const scale = ((nBarsToMask * (barHeight + barSpacing)) + (barSpacing / 2)) / height;
return (
<svg
className={styles.container}
height={height}
width={width}
<div
className={styles.maskContainer}
style={{height: `${height}px`}}
>
{Array(nBars).fill(0)
.map((value, index) => (
<rect
className={index < nGreen ? styles.green :
(index < nGreen + nYellow ? styles.yellow : styles.red)}
height={barHeight}
key={index}
rx={barRounding}
ry={barRounding}
width={width - 2}
x={1}
y={height - ((barSpacing + barHeight) * (index + 1))}
/>
))}
<rect
fill="hsla(215, 100%, 95%, 1)"
height={(nBarsToMask * (barHeight + barSpacing)) + (barSpacing / 2)}
opacity="0.75"
<svg
className={styles.container}
height={height}
width={width}
x={0}
y={0}
>
{Array(nBars).fill(0)
.map((value, index) => (
<rect
className={index < nGreen ? styles.green :
(index < nGreen + nYellow ? styles.yellow : styles.red)}
height={barHeight}
key={index}
rx={barRounding}
ry={barRounding}
width={width - 2}
x={1}
y={height - ((barSpacing + barHeight) * (index + 1))}
/>
))}
</svg>
<div
className={styles.mask}
style={{
transform: `scaleY(${scale})`
}}
/>
</svg>
</div>
);
};
+42 -22
View File
@@ -19,7 +19,8 @@ const categories = {
sound: '#CF63CF',
looks: '#9966FF',
motion: '#4C97FF',
list: '#FC662C'
list: '#FC662C',
extension: '#0FBD8C'
};
const modes = {
@@ -53,35 +54,52 @@ const MonitorComponent = props => (
})}
</Box>
</Draggable>
{props.mode === 'list' ? null : ReactDOM.createPortal((
{ReactDOM.createPortal((
// Use a portal to render the context menu outside the flow to avoid
// positioning conflicts between the monitors `transform: scale` and
// the context menus `position: fixed`. For more details, see
// http://meyerweb.com/eric/thoughts/2011/09/12/un-fixing-fixed-elements-with-css-transforms/
<ContextMenu id={`monitor-${props.label}`}>
<MenuItem onClick={props.onSetModeToDefault}>
<FormattedMessage
defaultMessage="normal readout"
description="Menu item to switch to the default monitor"
id="gui.monitor.contextMenu.default"
/>
</MenuItem>
<MenuItem onClick={props.onSetModeToLarge}>
<FormattedMessage
defaultMessage="large readout"
description="Menu item to switch to the large monitor"
id="gui.monitor.contextMenu.large"
/>
</MenuItem>
{props.onSetModeToSlider ? (
{props.onSetModeToDefault &&
<MenuItem onClick={props.onSetModeToDefault}>
<FormattedMessage
defaultMessage="normal readout"
description="Menu item to switch to the default monitor"
id="gui.monitor.contextMenu.default"
/>
</MenuItem>}
{props.onSetModeToLarge &&
<MenuItem onClick={props.onSetModeToLarge}>
<FormattedMessage
defaultMessage="large readout"
description="Menu item to switch to the large monitor"
id="gui.monitor.contextMenu.large"
/>
</MenuItem>}
{props.onSetModeToSlider &&
<MenuItem onClick={props.onSetModeToSlider}>
<FormattedMessage
defaultMessage="slider"
description="Menu item to switch to the slider monitor"
id="gui.monitor.contextMenu.slider"
/>
</MenuItem>
) : null}
</MenuItem>}
{props.onImport &&
<MenuItem onClick={props.onImport}>
<FormattedMessage
defaultMessage="import"
description="Menu item to import into list monitors"
id="gui.monitor.contextMenu.import"
/>
</MenuItem>}
{props.onExport &&
<MenuItem onClick={props.onExport}>
<FormattedMessage
defaultMessage="export"
description="Menu item to export from list monitors"
id="gui.monitor.contextMenu.export"
/>
</MenuItem>}
</ContextMenu>
), document.body)}
</ContextMenuTrigger>
@@ -99,14 +117,16 @@ MonitorComponent.propTypes = {
label: PropTypes.string.isRequired,
mode: PropTypes.oneOf(monitorModes),
onDragEnd: PropTypes.func.isRequired,
onExport: PropTypes.func,
onImport: PropTypes.func,
onNextMode: PropTypes.func.isRequired,
onSetModeToDefault: PropTypes.func.isRequired,
onSetModeToLarge: PropTypes.func.isRequired,
onSetModeToDefault: PropTypes.func,
onSetModeToLarge: PropTypes.func,
onSetModeToSlider: PropTypes.func
};
MonitorComponent.defaultProps = {
category: 'data',
category: 'extension',
mode: 'default'
};
+14 -24
View File
@@ -10,11 +10,20 @@
padding: 1.5rem 2.25rem;
}
.body input[type="checkbox"],
.body input[type="radio"] {
margin: 3px;
}
.label {
font-weight: 500;
margin: 0 0 0.75rem;
}
.disabled-label {
opacity: 0.5;
}
.variable-name-text-input {
margin-bottom: 1.5rem;
width: 100%;
@@ -68,35 +77,16 @@
margin-right: 0.5rem;
}
.more-options {
.cloud-option {
display:flex;
border-top: 1px dashed hsla(0, 0%, 0%, .25);
overflow: visible;
padding: 1rem;
padding: 1rem 0;
text-align: center;
margin: 0 0 1rem;
}
.more-options-accordion {
width: 60%;
width: 100%;
margin: 0 auto;
}
.more-options-text {
.cloud-option-text {
opacity: .5;
}
.more-options-icon {
width: .75rem;
height: .75rem;
vertical-align: middle;
padding-bottom: .2rem;
opacity: .5;
}
[dir="ltr"] .more-options-icon {
margin-left: .5rem;
}
[dir="rtl"] .more-options-icon {
margin-right: .5rem;
}
+39 -30
View File
@@ -1,14 +1,13 @@
import classNames from 'classnames';
import {defineMessages, FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import Box from '../box/box.jsx';
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import Modal from '../../containers/modal.jsx';
import styles from './prompt.css';
import dropdownIcon from './icon--dropdown-caret.svg';
const messages = defineMessages({
forAllSpritesMessage: {
@@ -21,10 +20,10 @@ const messages = defineMessages({
description: 'Option message when creating a varaible for making it only available to the current sprite',
id: 'gui.gui.variableScopeOptionSpriteOnly'
},
moreOptionsMessage: {
defaultMessage: 'More Options',
description: 'Dropdown message for variable/list options',
id: 'gui.gui.variablePrompt'
cloudVarOptionMessage: {
defaultMessage: 'Cloud variable (stored on server)',
description: 'Option message when creating a variable for making it a cloud variable, a variable that is stored on the server', /* eslint-disable-line max-len */
id: 'gui.gui.cloudVariableOption'
},
availableToAllSpritesMessage: {
defaultMessage: 'This variable will be available to all sprites.',
@@ -48,13 +47,14 @@ const PromptComponent = props => (
<input
autoFocus
className={styles.variableNameTextInput}
defaultValue={props.defaultValue}
name={props.label}
placeholder={props.placeholder}
onChange={props.onChange}
onFocus={props.onFocus}
onKeyPress={props.onKeyPress}
/>
</Box>
{props.showMoreOptions ?
{props.showVariableOptions ?
<div>
{props.isStage ?
<div className={styles.infoMessage}>
@@ -65,45 +65,48 @@ const PromptComponent = props => (
<Box className={styles.optionsRow}>
<label>
<input
defaultChecked
checked={props.globalSelected}
name="variableScopeOption"
type="radio"
value="global"
onChange={props.onOptionSelection}
onChange={props.onScopeOptionSelection}
/>
<FormattedMessage
{...messages.forAllSpritesMessage}
/>
</label>
<label>
<label
className={classNames({[styles.disabledLabel]: props.cloudSelected})}
>
<input
checked={!props.globalSelected}
disabled={props.cloudSelected}
name="variableScopeOption"
type="radio"
value="local"
onChange={props.onOptionSelection}
onChange={props.onScopeOptionSelection}
/>
<FormattedMessage
{...messages.forThisSpriteMessage}
/>
</label>
</Box>}
<Box className={styles.moreOptions}>
<ComingSoonTooltip
className={styles.moreOptionsAccordion}
place="right"
tooltipId="variable-options-accordion"
>
<div className={styles.moreOptionsText}>
{props.showCloudOption ?
<Box className={classNames(styles.cloudOption)}>
<label
className={classNames({[styles.disabledLabel]: !props.canAddCloudVariable})}
>
<input
checked={props.cloudSelected && props.canAddCloudVariable}
disabled={!props.canAddCloudVariable}
type="checkbox"
onChange={props.onCloudVarOptionChange}
/>
<FormattedMessage
{...messages.moreOptionsMessage}
{...messages.cloudVarOptionMessage}
/>
<img
className={styles.moreOptionsIcon}
src={dropdownIcon}
/>
</div>
</ComingSoonTooltip>
</Box>
</label>
</Box> : null}
</div> : null}
<Box className={styles.buttonRow}>
@@ -133,15 +136,21 @@ const PromptComponent = props => (
);
PromptComponent.propTypes = {
canAddCloudVariable: PropTypes.bool.isRequired,
cloudSelected: PropTypes.bool.isRequired,
defaultValue: PropTypes.string,
globalSelected: PropTypes.bool.isRequired,
isStage: PropTypes.bool.isRequired,
label: PropTypes.string.isRequired,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onCloudVarOptionChange: PropTypes.func,
onFocus: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onOk: PropTypes.func.isRequired,
onOptionSelection: PropTypes.func.isRequired,
placeholder: PropTypes.string,
showMoreOptions: PropTypes.bool.isRequired,
onScopeOptionSelection: PropTypes.func.isRequired,
showCloudOption: PropTypes.bool.isRequired,
showVariableOptions: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired
};
@@ -79,6 +79,7 @@ $border-radius: 0.25rem;
font-size: 0.85rem;
transition: 0.2s;
user-select: none;
margin: 0px;
}
.button > img {
+28 -9
View File
@@ -1,19 +1,20 @@
@import "../../css/colors.css";
.spinner {
width: 1rem;
height: 1rem;
width: 1.25rem;
height: 1.25rem;
display: inline-block;
position: relative;
border-radius: 50%;
border-width: .1875rem;
border-style: solid;
border-color: $ui-white-transparent;
box-sizing: content-box;
}
.spinner::after, .spinner::before {
width: .625rem;
height: .625rem;
.spinner::before, .spinner::after {
width: 1.25rem;
height: 1.25rem;
content: '';
border-radius: 50%;
display: block;
@@ -28,6 +29,16 @@
animation: spin 1.5s cubic-bezier(0.4, 0.1, 0.4, 1) infinite;
}
.small {
width: .5rem;
height: .5rem;
}
.small::before, .small::after {
width: .5rem;
height: .5rem;
}
@keyframes spin {
0% {
transform: rotate(0deg);
@@ -38,17 +49,25 @@
}
}
.spinner.orange {
.spinner.success {
border-color: $extensions-transparent;
}
.spinner.success::after {
border-top-color: $extensions-primary;
}
.spinner.warn {
border-color: $error-transparent;
}
.spinner.orange::after {
.spinner.warn::after {
border-top-color: $error-primary;
}
.spinner.white {
.spinner.info {
border-color: $ui-white-transparent;
}
.spinner.white::after {
.spinner.info::after {
border-top-color: $ui-white;
}
+11 -4
View File
@@ -6,21 +6,28 @@ import styles from './spinner.css';
const SpinnerComponent = function (props) {
const {
className
className,
level,
small
} = props;
return (
<div
className={classNames(
className,
styles.spinner,
className
styles[level],
{[styles.small]: small}
)}
/>
);
};
SpinnerComponent.propTypes = {
className: PropTypes.string
className: PropTypes.string,
level: PropTypes.string,
small: PropTypes.bool
};
SpinnerComponent.defaultProps = {
className: ''
className: '',
level: 'info'
};
export default SpinnerComponent;
+9 -8
View File
@@ -34,14 +34,15 @@ class SpriteInfo extends React.Component {
shouldComponentUpdate (nextProps) {
return (
this.props.rotationStyle !== nextProps.rotationStyle ||
this.props.direction !== nextProps.direction ||
this.props.disabled !== nextProps.disabled ||
this.props.name !== nextProps.name ||
this.props.size !== nextProps.size ||
this.props.stageSize !== nextProps.stageSize ||
this.props.visible !== nextProps.visible ||
this.props.x !== nextProps.x ||
this.props.y !== nextProps.y
// Only update these if rounded value has changed
Math.round(this.props.direction) !== Math.round(nextProps.direction) ||
Math.round(this.props.size) !== Math.round(nextProps.size) ||
Math.round(this.props.x) !== Math.round(nextProps.x) ||
Math.round(this.props.y) !== Math.round(nextProps.y)
);
}
render () {
@@ -110,7 +111,7 @@ class SpriteInfo extends React.Component {
placeholder="x"
tabIndex="0"
type="text"
value={this.props.disabled ? '' : this.props.x}
value={this.props.disabled ? '' : Math.round(this.props.x)}
onSubmit={this.props.onChangeX}
/>
</Label>
@@ -137,7 +138,7 @@ class SpriteInfo extends React.Component {
placeholder="y"
tabIndex="0"
type="text"
value={this.props.disabled ? '' : this.props.y}
value={this.props.disabled ? '' : Math.round(this.props.y)}
onSubmit={this.props.onChangeY}
/>
</Label>
@@ -237,14 +238,14 @@ class SpriteInfo extends React.Component {
label={sizeLabel}
tabIndex="0"
type="text"
value={this.props.disabled ? '' : this.props.size}
value={this.props.disabled ? '' : Math.round(this.props.size)}
onSubmit={this.props.onChangeSize}
/>
</Label>
</div>
<div className={classNames(styles.group, styles.largerInput)}>
<DirectionPicker
direction={this.props.direction}
direction={Math.round(this.props.direction)}
disabled={this.props.disabled}
labelAbove={labelAbove}
rotationStyle={this.props.rotationStyle}
@@ -18,7 +18,6 @@
text-align: center;
cursor: pointer;
transition: 0.25s ease-out;
user-select: none;
touch-action: none;
@@ -39,8 +38,24 @@
filter: drop-shadow(0px 0px 2px $ui-black-transparent);
}
/* Outer/Inner chicanery is to prevent layouts when sprite image changes */
.sprite-image-outer {
position: relative;
width: 100%;
height: 100%;
transform: translateZ(0);
}
.sprite-image-inner {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.sprite-image {
margin: auto;
user-select: none;
pointer-events: none;
max-width: 32px;
@@ -5,7 +5,7 @@ import React from 'react';
import CloseButton from '../close-button/close-button.jsx';
import styles from './sprite-selector-item.css';
import {ContextMenuTrigger} from 'react-contextmenu';
import {ContextMenu, MenuItem} from '../context-menu/context-menu.jsx';
import {BorderedMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx';
import {FormattedMessage} from 'react-intl';
// react-contextmenu requires unique id to match trigger and context menu
@@ -26,22 +26,19 @@ const SpriteSelectorItem = props => (
disable={props.dragging}
id={`${props.name}-${contextMenuId}`}
>
{(props.selected && props.onDeleteButtonClick) ? (
<CloseButton
className={styles.deleteButton}
size={CloseButton.SIZE_SMALL}
onClick={props.onDeleteButtonClick}
/>
) : null }
{typeof props.number === 'undefined' ? null : (
<div className={styles.number}>{props.number}</div>
)}
{props.costumeURL ? (
<img
className={styles.spriteImage}
draggable={false}
src={props.costumeURL}
/>
<div className={styles.spriteImageOuter}>
<div className={styles.spriteImageInner}>
<img
className={styles.spriteImage}
draggable={false}
src={props.costumeURL}
/>
</div>
</div>
) : null}
<div className={styles.spriteInfo}>
<div className={styles.spriteName}>{props.name}</div>
@@ -49,6 +46,13 @@ const SpriteSelectorItem = props => (
<div className={styles.spriteDetails}>{props.details}</div>
) : null}
</div>
{(props.selected && props.onDeleteButtonClick) ? (
<CloseButton
className={styles.deleteButton}
size={CloseButton.SIZE_SMALL}
onClick={props.onDeleteButtonClick}
/>
) : null }
{props.onDuplicateButtonClick || props.onDeleteButtonClick || props.onExportButtonClick ? (
<ContextMenu id={`${props.name}-${contextMenuId++}`}>
{props.onDuplicateButtonClick ? (
@@ -60,15 +64,6 @@ const SpriteSelectorItem = props => (
/>
</MenuItem>
) : null}
{props.onDeleteButtonClick ? (
<MenuItem onClick={props.onDeleteButtonClick}>
<FormattedMessage
defaultMessage="delete"
description="Menu item to delete in the right click menu"
id="gui.spriteSelectorItem.contextMenuDelete"
/>
</MenuItem>
) : null }
{props.onExportButtonClick ? (
<MenuItem onClick={props.onExportButtonClick}>
<FormattedMessage
@@ -78,6 +73,15 @@ const SpriteSelectorItem = props => (
/>
</MenuItem>
) : null }
{props.onDeleteButtonClick ? (
<BorderedMenuItem onClick={props.onDeleteButtonClick}>
<FormattedMessage
defaultMessage="delete"
description="Menu item to delete in the right click menu"
id="gui.spriteSelectorItem.contextMenuDelete"
/>
</BorderedMenuItem>
) : null }
</ContextMenu>
) : null}
</ContextMenuTrigger>
+57 -48
View File
@@ -8,9 +8,12 @@ import Box from '../box/box.jsx';
import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
import SortableHOC from '../../lib/sortable-hoc.jsx';
import SortableAsset from '../asset-panel/sortable-asset.jsx';
import ThrottledPropertyHOC from '../../lib/throttled-property-hoc.jsx';
import styles from './sprite-selector.css';
const ThrottledSpriteSelectorItem = ThrottledPropertyHOC('asset', 500)(SpriteSelectorItem);
const SpriteList = function (props) {
const {
containerRef,
@@ -34,62 +37,68 @@ const SpriteList = function (props) {
return (
<Box
className={styles.itemsWrapper}
className={classNames(styles.scrollWrapper, {
[styles.scrollWrapperDragging]: draggingType === DragConstants.BACKPACK_SPRITE
})}
componentRef={containerRef}
>
{items.map((sprite, index) => {
<Box
className={styles.itemsWrapper}
>
{items.map((sprite, index) => {
// If the sprite has just received a block drop, used for green highlight
const receivedBlocks = (
hoveredTarget.sprite === sprite.id &&
// If the sprite has just received a block drop, used for green highlight
const receivedBlocks = (
hoveredTarget.sprite === sprite.id &&
sprite.id !== editingTarget &&
hoveredTarget.receivedBlocks
);
);
// If the sprite is indicating it can receive block dropping, used for blue highlight
let isRaised = !receivedBlocks && raised && sprite.id !== editingTarget;
// If the sprite is indicating it can receive block dropping, used for blue highlight
let isRaised = !receivedBlocks && raised && sprite.id !== editingTarget;
// A sprite is also raised if a costume or sound is being dragged.
// 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 || [
DragConstants.COSTUME,
DragConstants.SOUND,
DragConstants.BACKPACK_COSTUME,
DragConstants.BACKPACK_SOUND,
DragConstants.BACKPACK_CODE].includes(draggingType);
// A sprite is also raised if a costume or sound is being dragged.
// 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 || [
DragConstants.COSTUME,
DragConstants.SOUND,
DragConstants.BACKPACK_COSTUME,
DragConstants.BACKPACK_SOUND,
DragConstants.BACKPACK_CODE].includes(draggingType);
return (
<SortableAsset
className={classNames(styles.spriteWrapper, {
[styles.placeholder]: isSpriteDrag && index === draggingIndex})}
index={isSpriteDrag ? ordering.indexOf(index) : index}
key={sprite.name}
onAddSortable={onAddSortable}
onRemoveSortable={onRemoveSortable}
>
<SpriteSelectorItem
asset={sprite.costume && sprite.costume.asset}
className={classNames(styles.sprite, {
[styles.raised]: isRaised,
[styles.receivedBlocks]: receivedBlocks
})}
dragPayload={sprite}
dragType={DragConstants.SPRITE}
id={sprite.id}
index={index}
key={sprite.id}
name={sprite.name}
selected={sprite.id === selectedId}
onClick={onSelectSprite}
onDeleteButtonClick={onDeleteSprite}
onDuplicateButtonClick={onDuplicateSprite}
onExportButtonClick={onExportSprite}
/>
</SortableAsset>
);
})}
return (
<SortableAsset
className={classNames(styles.spriteWrapper, {
[styles.placeholder]: isSpriteDrag && index === draggingIndex})}
index={isSpriteDrag ? ordering.indexOf(index) : index}
key={sprite.name}
onAddSortable={onAddSortable}
onRemoveSortable={onRemoveSortable}
>
<ThrottledSpriteSelectorItem
asset={sprite.costume && sprite.costume.asset}
className={classNames(styles.sprite, {
[styles.raised]: isRaised,
[styles.receivedBlocks]: receivedBlocks
})}
dragPayload={sprite.id}
dragType={DragConstants.SPRITE}
id={sprite.id}
index={index}
key={sprite.id}
name={sprite.name}
selected={sprite.id === selectedId}
onClick={onSelectSprite}
onDeleteButtonClick={onDeleteSprite}
onDuplicateButtonClick={onDuplicateSprite}
onExportButtonClick={onExportSprite}
/>
</SortableAsset>
);
})}
</Box>
</Box>
);
};
@@ -31,6 +31,7 @@
*/
box-sizing: border-box;
width: calc((100% / $sprites-per-row ) - $space);
max-width: 6rem;
min-width: 4rem;
min-height: 4rem; /* @todo: calc height same as width */
margin: calc($space / 2);
@@ -54,6 +55,10 @@
overflow-y: auto;
}
.scroll-wrapper-dragging {
background-color: $drop-highlight;
}
.items-wrapper {
display: flex;
flex-wrap: wrap;
@@ -79,12 +84,12 @@
}
.raised {
background-color: #8cbcff;
background-color: $drop-highlight;
transition: all 0.25s ease;
}
.raised:hover {
background-color: #8cbcff;
background-color: $drop-highlight;
transform: scale(1.05);
}
@@ -93,7 +98,7 @@
animation-duration: 500ms;
animation-iteration-count: 1;
animation-timing-function: ease-in-out;
background-color: #8cbcff;
background-color: $drop-highlight;
}
@keyframes wiggle {
@@ -100,20 +100,18 @@ const SpriteSelectorComponent = function (props) {
onChangeY={onChangeSpriteY}
/>
<Box className={styles.scrollWrapper}>
<SpriteList
editingTarget={editingTarget}
hoveredTarget={hoveredTarget}
items={Object.keys(sprites).map(id => sprites[id])}
raised={raised}
selectedId={selectedId}
onDeleteSprite={onDeleteSprite}
onDrop={onDrop}
onDuplicateSprite={onDuplicateSprite}
onExportSprite={onExportSprite}
onSelectSprite={onSelectSprite}
/>
</Box>
<SpriteList
editingTarget={editingTarget}
hoveredTarget={hoveredTarget}
items={Object.keys(sprites).map(id => sprites[id])}
raised={raised}
selectedId={selectedId}
onDeleteSprite={onDeleteSprite}
onDrop={onDrop}
onDuplicateSprite={onDuplicateSprite}
onExportSprite={onExportSprite}
onSelectSprite={onSelectSprite}
/>
<ActionMenu
className={styles.addButton}
img={spriteIcon}
+7 -1
View File
@@ -26,6 +26,12 @@
padding-bottom: $space;
}
.embed-scratch-logo img {
height: 1.6rem;
vertical-align: middle;
opacity: .6;
}
.stage-size-row {
display: flex;
}
@@ -45,7 +51,7 @@
.stage-button {
display: block;
border: 1px solid $ui-black-transparent;
border-radius: .25rem;
border-radius: $form-radius;
width: calc(2rem + 2px);
height: calc(2rem + 2px);
background: $ui-white;
+32 -13
View File
@@ -16,6 +16,7 @@ import largeStageIcon from './icon--large-stage.svg';
import smallStageIcon from './icon--small-stage.svg';
import unFullScreenIcon from './icon--unfullscreen.svg';
import scratchLogo from '../menu-bar/scratch-logo.svg';
import styles from './stage-header.css';
const messages = defineMessages({
@@ -55,6 +56,7 @@ const StageHeaderComponent = function (props) {
onSetStageSmall,
onSetStageFull,
onSetStageUnFull,
showBranding,
stageSizeMode,
vm
} = props;
@@ -63,6 +65,34 @@ const StageHeaderComponent = function (props) {
if (isFullScreen) {
const stageDimensions = getStageDimensions(null, true);
const stageButton = showBranding ? (
<div className={styles.embedScratchLogo}>
<a
href="https://scratch.mit.edu"
rel="noopener noreferrer"
target="_blank"
>
<img
alt="Scratch"
src={scratchLogo}
/>
</a>
</div>
) : (
<Button
className={styles.stageButton}
onClick={onSetStageUnFull}
onKeyPress={onKeyPress}
>
<img
alt={props.intl.formatMessage(messages.unFullStageSizeMessage)}
className={styles.stageButtonIcon}
draggable={false}
src={unFullScreenIcon}
title={props.intl.formatMessage(messages.fullscreenControl)}
/>
</Button>
);
header = (
<Box className={styles.stageHeaderWrapperOverlay}>
<Box
@@ -70,19 +100,7 @@ const StageHeaderComponent = function (props) {
style={{width: stageDimensions.width}}
>
<Controls vm={vm} />
<Button
className={styles.stageButton}
onClick={onSetStageUnFull}
onKeyPress={onKeyPress}
>
<img
alt={props.intl.formatMessage(messages.unFullStageSizeMessage)}
className={styles.stageButtonIcon}
draggable={false}
src={unFullScreenIcon}
title={props.intl.formatMessage(messages.fullscreenControl)}
/>
</Button>
{stageButton}
</Box>
</Box>
);
@@ -171,6 +189,7 @@ StageHeaderComponent.propTypes = {
onSetStageLarge: PropTypes.func.isRequired,
onSetStageSmall: PropTypes.func.isRequired,
onSetStageUnFull: PropTypes.func.isRequired,
showBranding: PropTypes.bool.isRequired,
stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)),
vm: PropTypes.instanceOf(VM).isRequired
};
@@ -100,7 +100,7 @@ $header-height: calc($stage-menu-height - 2px);
}
.raised, .raised .header {
background-color: #8cbcff;
background-color: $drop-highlight;
transition: all 0.25s ease;
}
+14 -1
View File
@@ -6,18 +6,25 @@ import Box from '../box/box.jsx';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
import StageHeader from '../../containers/stage-header.jsx';
import Stage from '../../containers/stage.jsx';
import Loader from '../loader/loader.jsx';
import styles from './stage-wrapper.css';
const StageWrapperComponent = function (props) {
const {
isFullScreen,
isRtl,
isRendererSupported,
loading,
stageSize,
vm
} = props;
return (
<Box className={styles.stageWrapper}>
<Box
className={styles.stageWrapper}
dir={isRtl ? 'rtl' : 'ltr'}
>
<Box className={styles.stageMenuWrapper}>
<StageHeader
stageSize={stageSize}
@@ -34,12 +41,18 @@ const StageWrapperComponent = function (props) {
null
}
</Box>
{loading ? (
<Loader isFullScreen={isFullScreen} />
) : null}
</Box>
);
};
StageWrapperComponent.propTypes = {
isFullScreen: PropTypes.bool,
isRendererSupported: PropTypes.bool.isRequired,
isRtl: PropTypes.bool.isRequired,
loading: PropTypes.bool,
stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired,
vm: PropTypes.instanceOf(VM).isRequired
};
+35 -2
View File
@@ -9,9 +9,9 @@
*/
display: block;
/* Attach border radius directly to canvas to prevent needing overflow:hidden; */
border-radius: $space;
border: $stage-standard-border-width solid $ui-black-transparent;
overflow: hidden;
/* @todo: This is for overriding the value being set somewhere. Where is it being set? */
background-color: transparent;
@@ -89,7 +89,10 @@ to adjust for the border using a different method */
padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
}
.monitor-wrapper, .color-picker-wrapper, .frame-wrapper {
.monitor-wrapper,
.color-picker-wrapper,
.frame-wrapper,
.green-flag-overlay-wrapper {
position: absolute;
top: 0;
left: 0;
@@ -138,6 +141,36 @@ to adjust for the border using a different method */
animation-fill-mode: forwards; /* Leave at 0 opacity after animation */
}
.green-flag-overlay-wrapper {
display: flex;
justify-content: center;
align-items: center;
background: rgba(0,0,0,0.25);
border-radius: 0.5rem;
pointer-events: all;
cursor: pointer;
}
.green-flag-overlay {
padding: 1rem;
border-radius: 100%;
background: rgba(255,255,255,0.75);
border: 3px solid $ui-white;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: 5rem;
height: 5rem;
}
.green-flag-overlay > img {
width: 100%;
object-fit: contain;
}
@keyframes flash {
0% { opacity: 1; }
100% { opacity: 0; }
+20 -4
View File
@@ -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 TargetHighlight from '../../containers/target-highlight.jsx';
import GreenFlagOverlay from '../../containers/green-flag-overlay.jsx';
import Question from '../../containers/question.jsx';
import MicIndicator from '../mic-indicator/mic-indicator.jsx';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
@@ -19,6 +20,7 @@ const StageComponent = props => {
dragRef,
isColorPicking,
isFullScreen,
isStarted,
colorInfo,
micIndicator,
question,
@@ -46,18 +48,25 @@ const StageComponent = props => {
}}
onDoubleClick={onDoubleClick}
>
<DOMElementRenderer
<Box
className={classNames(
styles.stage,
{[styles.stageOverlayContent]: isFullScreen}
)}
domElement={canvas}
style={{
height: stageDimensions.height,
width: stageDimensions.width
}}
{...boxProps}
/>
>
<DOMElementRenderer
domElement={canvas}
style={{
height: stageDimensions.height,
width: stageDimensions.width
}}
{...boxProps}
/>
</Box>
<Box className={styles.monitorWrapper}>
<MonitorList
draggable={useEditorDragStyle}
@@ -71,6 +80,12 @@ const StageComponent = props => {
stageWidth={stageDimensions.width}
/>
</Box>
{isStarted ? null : (
<GreenFlagOverlay
className={styles.greenFlagOverlay}
wrapperClass={styles.greenFlagOverlayWrapper}
/>
)}
{isColorPicking && colorInfo ? (
<Box className={styles.colorPickerWrapper}>
<Loupe colorInfo={colorInfo} />
@@ -125,6 +140,7 @@ StageComponent.propTypes = {
dragRef: PropTypes.func,
isColorPicking: PropTypes.bool,
isFullScreen: PropTypes.bool.isRequired,
isStarted: PropTypes.bool,
micIndicator: PropTypes.bool,
onDeactivateColorPicker: PropTypes.func,
onDoubleClick: PropTypes.func,
+6 -5
View File
@@ -1,3 +1,5 @@
@import "../../css/colors.css";
.stop-all {
width: 2rem;
height: 2rem;
@@ -5,17 +7,16 @@
border-radius: 0.25rem;
user-select: none;
cursor: pointer;
transition: 0.2s ease-out;
}
.stop-all:hover {
background-color: $motion-light-transparent;
}
.stop-all {
opacity: 0.5;
}
.stop-all:hover {
transform: scale(1.2);
}
.stop-all.is-active {
opacity: 1;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

@@ -0,0 +1,82 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/typography.css";
@import "../../css/z-index.css";
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $z-index-modal;
background-color: $ui-modal-overlay;
}
.modal-content {
margin: 100px auto;
outline: none;
border: .25rem solid $ui-white-transparent;
padding: 0;
border-radius: $space;
user-select: none;
width: 640px;
color: $text-primary;
overflow: hidden;
}
.illustration {
width: 100%;
height: 123px;
background-color: $motion-primary;
background-image: url('./telemetry-modal-header.png');
background-size: cover;
}
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
text-align: left;
}
.privacy-policy-link {
color: $motion-primary;
text-decoration: none;
}
/* Confirmation buttons at the bottom of the modal */
.button-row {
margin: 1.5rem 0;
font-weight: bolder;
text-align: right;
display: flex;
justify-content: center;
}
.button-row button {
border: 1px solid $motion-primary;
border-radius: 0.25rem;
padding: 0.5rem 1.5rem;
background: white;
font-weight: bold;
font-size: .875rem;
cursor: pointer;
}
.button-row button.opt-in {
background: $motion-primary;
color: white;
}
.button-row button.opt-out {
color: $motion-primary;
}
[dir="ltr"] .button-row button + button {
margin-left: 0.5rem;
}
[dir="rtl"] .button-row button + button {
margin-right: 0.5rem;
}
@@ -0,0 +1,140 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
import ReactModal from 'react-modal';
import Box from '../box/box.jsx';
import styles from './telemetry-modal.css';
const messages = defineMessages({
label: {
id: 'gui.telemetryOptIn.label',
defaultMessage: 'Report statistics to improve Scratch',
description: 'Scratch 3.0 telemetry modal label - for accessibility'
},
bodyText1: {
defaultMessage: 'The Scratch Team is always looking to better understand how Scratch is used around the ' +
'world. To help support this effort, you can allow Scratch to automatically send usage information to ' +
'the Scratch Team.',
description: 'First paragraph of body text for telemetry opt-in modal',
id: 'gui.telemetryOptIn.body1'
},
bodyText2: {
defaultMessage: 'The information we collect includes language selection, blocks usage, and some events like ' +
'saving, loading, and uploading a project. We DO NOT collect any personal information. Please see our ' +
'{privacyPolicyLink} for more information.',
description: 'First paragraph of body text for telemetry opt-in modal',
id: 'gui.telemetryOptIn.body2'
},
privacyPolicyLink: {
defaultMessage: 'Privacy Policy',
description: 'Link to the Scratch privacy policy',
id: 'gui.telemetryOptIn.privacyPolicyLink'
},
noButton: {
defaultMessage: 'No, thanks',
description: 'Text for telemetry modal opt-out button',
id: 'gui.telemetryOptIn.buttonTextNo'
},
noTooltip: {
defaultMessage: 'Disable telemetry',
description: 'Tooltip for telemetry modal opt-out button',
id: 'gui.telemetryOptIn.buttonTooltipNo'
},
yesButton: {
defaultMessage: "Yes, I'd like to help improve Scratch",
description: 'Text for telemetry modal opt-in button',
id: 'gui.telemetryOptIn.buttonTextYes'
},
yesTooltip: {
defaultMessage: 'Enable telemetry',
description: 'Tooltip for telemetry modal opt-in button',
id: 'gui.telemetryOptIn.buttonTooltipYes'
}
});
class TelemetryModal extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'handleCancel',
'handleOptIn',
'handleOptOut'
]);
}
handleCancel () {
this.props.onRequestClose();
if (this.props.onCancel) {
this.props.onCancel();
}
}
handleOptIn () {
this.props.onRequestClose();
if (this.props.onOptIn) {
this.props.onOptIn();
}
}
handleOptOut () {
this.props.onRequestClose();
if (this.props.onOptOut) {
this.props.onOptOut();
}
}
render () {
return (<ReactModal
isOpen
className={styles.modalContent}
contentLabel={this.props.intl.formatMessage(messages.label)}
overlayClassName={styles.modalOverlay}
onRequestClose={this.handleCancel}
>
<div dir={this.props.isRtl ? 'rtl' : 'ltr'} >
<Box className={styles.illustration} />
<Box className={styles.body}>
<p><FormattedMessage {...messages.bodyText1} /></p>
<p><FormattedMessage
{...messages.bodyText2}
values={{
privacyPolicyLink: (<a
className={styles.privacyPolicyLink}
href="https://scratch.mit.edu/privacy_policy/"
>
<FormattedMessage {...messages.privacyPolicyLink} />
</a>)
}}
/></p>
<Box className={styles.buttonRow}>
<button
className={styles.optOut}
title={this.props.intl.formatMessage(messages.noTooltip)}
onClick={this.handleOptOut}
>
<FormattedMessage {...messages.noButton} />
</button>
<button
className={styles.optIn}
title={this.props.intl.formatMessage(messages.yesTooltip)}
onClick={this.handleOptIn}
>
<FormattedMessage {...messages.yesButton} />
</button>
</Box>
</Box>
</div>
</ReactModal>);
}
}
TelemetryModal.propTypes = {
intl: intlShape.isRequired,
isRtl: PropTypes.bool,
onCancel: PropTypes.func,
onOptIn: PropTypes.func.isRequired,
onOptOut: PropTypes.func.isRequired,
onRequestClose: PropTypes.func
};
export default injectIntl(TelemetryModal);
+54 -41
View File
@@ -2,52 +2,65 @@ import React from 'react';
import PropTypes from 'prop-types';
import styles from './waveform.css';
const Waveform = props => {
const {
width,
height,
data
} = props;
// Waveform is expensive to compute, make sure it only updates when data does
// by using PureComponent. In future can be changed back to function with React.memo
// eslint-disable-next-line react/prefer-stateless-function
class Waveform extends React.PureComponent {
render () {
const {
width,
height,
data
} = this.props;
const cappedData = [0, ...data, 0];
// Never want a density of points higher than the number of pixels
// This is very conservative, could be far fewer points because of curve smoothing.
// Drawing too many points seems to cause an explosion in browser
// composite time when animating the playhead
const takeEveryN = Math.ceil(data.length / width);
const points = [
...cappedData.map((v, i) =>
[width * i / cappedData.length, height * v / 2]
),
...cappedData.reverse().map((v, i) =>
[width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2]
)
];
const filteredData = takeEveryN === 1 ? data :
data.filter((_, i) => i % takeEveryN === 0);
const pathComponents = points.map(([x, y], i) => {
const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0];
return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`;
});
const cappedData = [0, ...filteredData, 0];
return (
<svg
className={styles.container}
viewBox={`-1 0 ${width} ${height}`}
>
<line
className={styles.baseline}
x1={-1}
x2={width}
y1={height / 2}
y2={height / 2}
/>
<g transform={`scale(1, -1) translate(0, -${height / 2})`}>
<path
className={styles.waveformPath}
d={`M0 0${pathComponents.join(' ')}Z`}
strokeLinejoin={'round'}
strokeWidth={1}
const points = [
...cappedData.map((v, i) =>
[width * i / cappedData.length, height * v / 2]
),
...cappedData.reverse().map((v, i) =>
[width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2]
)
];
const pathComponents = points.map(([x, y], i) => {
const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0];
return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`;
});
return (
<svg
className={styles.container}
viewBox={`-1 0 ${width} ${height}`}
>
<line
className={styles.baseline}
x1={-1}
x2={width}
y1={height / 2}
y2={height / 2}
/>
</g>
</svg>
);
};
<g transform={`scale(1, -1) translate(0, -${height / 2})`}>
<path
className={styles.waveformPath}
d={`M0 0${pathComponents.join(' ')}Z`}
strokeLinejoin={'round'}
strokeWidth={1}
/>
</g>
</svg>
);
}
}
Waveform.propTypes = {
data: PropTypes.arrayOf(PropTypes.number),
+1 -1
View File
@@ -30,7 +30,7 @@
width: 100%;
height: 208px;
background-color: $motion-primary;
background-image: url('../browser-modal/unsupported.png');
background-image: url('./unsupported.png');
background-size: cover;
}
+1 -1
View File
@@ -39,7 +39,7 @@ const WebGlModal = ({intl, ...props}) => (
webGlLink: (
<a
className={styles.faqLink}
href="https://en.wikipedia.org/wiki/WebGL#Support"
href="https://get.webgl.org/"
>
<FormattedMessage
defaultMessage="does not support WebGL"
+42 -13
View File
@@ -2,10 +2,11 @@ import React from 'react';
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import SB3Downloader from './sb3-downloader.jsx';
import AlertComponent from '../components/alerts/alert.jsx';
import {openConnectionModal} from '../reducers/modals';
import {setConnectionModalExtensionId} from '../reducers/connection-modal';
import {manualUpdateProject} from '../reducers/project-state';
class Alert extends React.Component {
constructor (props) {
@@ -24,42 +25,70 @@ class Alert extends React.Component {
}
render () {
const {
closeButton,
content,
extensionName,
index, // eslint-disable-line no-unused-vars
level,
iconSpinner,
iconURL,
message,
showReconnect
onSaveNow,
showDownload,
showReconnect,
showSaveNow
} = this.props;
return (
<AlertComponent
iconURL={iconURL}
message={message}
showReconnect={showReconnect}
onCloseAlert={this.handleOnCloseAlert}
onReconnect={this.handleOnReconnect}
/>
<SB3Downloader>{(_, downloadProject) => (
<AlertComponent
closeButton={closeButton}
content={content}
extensionName={extensionName}
iconSpinner={iconSpinner}
iconURL={iconURL}
level={level}
message={message}
showDownload={showDownload}
showReconnect={showReconnect}
showSaveNow={showSaveNow}
onCloseAlert={this.handleOnCloseAlert}
onDownload={downloadProject}
onReconnect={this.handleOnReconnect}
onSaveNow={onSaveNow}
/>
)}</SB3Downloader>
);
}
}
const mapStateToProps = state => ({
state: state
});
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch => ({
onOpenConnectionModal: id => {
dispatch(setConnectionModalExtensionId(id));
dispatch(openConnectionModal());
},
onSaveNow: () => {
dispatch(manualUpdateProject());
}
});
Alert.propTypes = {
closeButton: PropTypes.bool,
content: PropTypes.element,
extensionId: PropTypes.string,
extensionName: PropTypes.string,
iconSpinner: PropTypes.bool,
iconURL: PropTypes.string,
index: PropTypes.number,
level: PropTypes.string.isRequired,
message: PropTypes.string,
onCloseAlert: PropTypes.func.isRequired,
onOpenConnectionModal: PropTypes.func,
showReconnect: PropTypes.bool.isRequired
onSaveNow: PropTypes.func,
showDownload: PropTypes.bool,
showReconnect: PropTypes.bool,
showSaveNow: PropTypes.bool
};
export default connect(
+4 -2
View File
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {
closeAlert
closeAlert,
filterPopupAlerts
} from '../reducers/alerts';
import AlertsComponent from '../components/alerts/alerts.jsx';
@@ -14,7 +15,8 @@ const Alerts = ({
onCloseAlert
}) => (
<AlertsComponent
alertsList={alertsList}
// only display standard and extension alerts here
alertsList={filterPopupAlerts(alertsList)}
className={className}
onCloseAlert={onCloseAlert}
/>
+1 -10
View File
@@ -5,11 +5,6 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {connect} from 'react-redux';
import VM from 'scratch-vm';
import {
activateTab,
COSTUMES_TAB_INDEX
} from '../reducers/editor-tab';
import analytics from '../lib/analytics';
import backdropLibraryContent from '../lib/libraries/backdrops.json';
import backdropTags from '../lib/libraries/backdrop-tags';
@@ -40,7 +35,6 @@ class BackdropLibrary extends React.Component {
skinId: null
};
this.props.vm.setEditingTarget(this.props.stageID);
this.props.onActivateTab(COSTUMES_TAB_INDEX);
this.props.vm.addBackdrop(item.md5, vmBackdrop);
analytics.event({
category: 'library',
@@ -64,7 +58,6 @@ class BackdropLibrary extends React.Component {
BackdropLibrary.propTypes = {
intl: intlShape.isRequired,
onActivateTab: PropTypes.func.isRequired,
onRequestClose: PropTypes.func,
stageID: PropTypes.string.isRequired,
vm: PropTypes.instanceOf(VM).isRequired
@@ -74,9 +67,7 @@ const mapStateToProps = state => ({
stageID: state.scratchGui.targets.stage.id
});
const mapDispatchToProps = dispatch => ({
onActivateTab: tab => dispatch(activateTab(tab))
});
const mapDispatchToProps = () => ({});
export default injectIntl(connect(
mapStateToProps,
+69 -28
View File
@@ -7,7 +7,6 @@ import React from 'react';
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 BlocksComponent from '../components/blocks/blocks.jsx';
@@ -51,6 +50,7 @@ class Blocks extends React.Component {
bindAll(this, [
'attachVM',
'detachVM',
'getToolboxXML',
'handleCategorySelected',
'handleConnectionModalStart',
'handleDrop',
@@ -95,6 +95,11 @@ class Blocks extends React.Component {
);
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
// Store the xml of the toolbox that is actually rendered.
// This is used in componentDidUpdate instead of prevProps, because
// the xml can change while e.g. on the costumes tab.
this._renderedToolboxXML = this.props.toolboxXML;
// we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the
// entire toolbox every time we reset the workspace. We call updateToolbox as a part of
// componentDidUpdate so the toolbox will still correctly be updated
@@ -113,14 +118,12 @@ class Blocks extends React.Component {
if (this.props.isVisible) {
this.setLocale();
}
analytics.pageview('/editors/blocks');
}
shouldComponentUpdate (nextProps, nextState) {
return (
this.state.prompt !== nextState.prompt ||
this.props.isVisible !== nextProps.isVisible ||
this.props.toolboxXML !== nextProps.toolboxXML ||
this._renderedToolboxXML !== nextProps.toolboxXML ||
this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible ||
this.props.customProceduresVisible !== nextProps.customProceduresVisible ||
this.props.locale !== nextProps.locale ||
@@ -134,13 +137,17 @@ class Blocks extends React.Component {
this.ScratchBlocks.hideChaff();
}
if (prevProps.toolboxXML !== this.props.toolboxXML) {
// Only rerender the toolbox when the blocks are visible and the xml is
// different from the previously rendered toolbox xml.
// Do not check against prevProps.toolboxXML because that may not have been rendered.
if (this.props.isVisible && this.props.toolboxXML !== this._renderedToolboxXML) {
// rather than update the toolbox "sync" -- update it in the next frame
clearTimeout(this.toolboxUpdateTimeout);
this.toolboxUpdateTimeout = setTimeout(() => {
this.updateToolbox();
}, 0);
}
if (this.props.isVisible === prevProps.isVisible) {
if (this.props.stageSize !== prevProps.stageSize) {
// force workspace to redraw for the new stage size
@@ -158,7 +165,6 @@ class Blocks extends React.Component {
this.setLocale();
} else {
this.props.vm.refreshWorkspace();
this.updateToolbox();
}
window.dispatchEvent(new Event('resize'));
@@ -173,13 +179,16 @@ class Blocks extends React.Component {
}
setLocale () {
this.workspace.getFlyout().setRecyclingEnabled(false);
this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
this.props.vm.setLocale(this.props.locale, this.props.messages)
.then(() => {
this.workspace.getFlyout().setRecyclingEnabled(false);
this.props.vm.refreshWorkspace();
this.updateToolbox();
this.workspace.getFlyout().setRecyclingEnabled(true);
// refreshWorkspace will cause a toolbox update
// wait for update to go through before reenabling recycling
this.withToolboxUpdates(() => {
this.workspace.getFlyout().setRecyclingEnabled(true);
});
});
}
@@ -189,6 +198,8 @@ class Blocks extends React.Component {
const categoryId = this.workspace.toolbox_.getSelectedCategoryId();
const offset = this.workspace.toolbox_.getCategoryScrollOffset();
this.workspace.updateToolbox(this.props.toolboxXML);
this._renderedToolboxXML = this.props.toolboxXML;
// In order to catch any changes that mutate the toolbox during "normal runtime"
// (variable changes/etc), re-enable toolbox refresh.
// Using the setter function will rerender the entire toolbox which we just rendered.
@@ -233,7 +244,7 @@ class Blocks extends React.Component {
this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded);
this.props.vm.addListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate);
this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate);
this.props.vm.addListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate);
this.props.vm.addListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate);
}
detachVM () {
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
@@ -246,7 +257,7 @@ class Blocks extends React.Component {
this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded);
this.props.vm.removeListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate);
this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate);
this.props.vm.removeListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate);
this.props.vm.removeListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate);
}
updateToolboxBlockValue (id, value) {
@@ -262,7 +273,7 @@ class Blocks extends React.Component {
}
onTargetsUpdate () {
if (this.props.vm.editingTarget) {
if (this.props.vm.editingTarget && this.workspace.getFlyout()) {
['glide', 'move', 'set'].forEach(prefix => {
this.updateToolboxBlockValue(`${prefix}x`, Math.round(this.props.vm.editingTarget.x).toString());
this.updateToolboxBlockValue(`${prefix}y`, Math.round(this.props.vm.editingTarget.y).toString());
@@ -297,12 +308,32 @@ class Blocks extends React.Component {
onVisualReport (data) {
this.workspace.reportValue(data.id, data.value);
}
getToolboxXML () {
// Use try/catch because this requires digging pretty deep into the VM
// Code inside intentionally ignores several error situations (no stage, etc.)
// Because they would get caught by this try/catch
try {
let {editingTarget: target, runtime} = this.props.vm;
const stage = runtime.getTargetForStage();
if (!target) target = stage; // If no editingTarget, use the stage
const stageCostumes = stage.getCostumes();
const targetCostumes = target.getCostumes();
const targetSounds = target.getSounds();
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
targetCostumes[0].name,
stageCostumes[0].name,
targetSounds.length > 0 ? targetSounds[0].name : ''
);
} catch {
return null;
}
}
onWorkspaceUpdate (data) {
// When we change sprites, update the toolbox to have the new sprite's blocks
if (this.props.vm.editingTarget) {
const target = this.props.vm.editingTarget;
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML);
const toolboxXML = this.getToolboxXML();
if (toolboxXML) {
this.props.updateToolboxState(toolboxXML);
}
@@ -325,7 +356,9 @@ class Blocks extends React.Component {
// 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}`;
if (error.message) {
error.message = `Workspace Update Error: ${error.message}`;
}
log.error(error);
}
this.workspace.addChangeListener(this.props.vm.blockListener);
@@ -348,12 +381,9 @@ class Blocks extends React.Component {
// this actually defines blocks and MUST run regardless of the UI state
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x));
// update the toolbox view: this can be skipped if we're not looking at a target, etc.
const runtime = this.props.vm.runtime;
const target = runtime.getEditingTarget() || runtime.getTargetForStage();
if (target) {
const dynamicBlocksXML = runtime.getBlocksXML();
const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML);
// Update the toolbox with new blocks
const toolboxXML = this.getToolboxXML();
if (toolboxXML) {
this.props.updateToolboxState(toolboxXML);
}
}
@@ -380,10 +410,11 @@ class Blocks extends React.Component {
this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE;
p.prompt.varType = typeof optVarType === 'string' ?
optVarType : this.ScratchBlocks.SCALAR_VARIABLE_TYPE;
p.prompt.showMoreOptions =
p.prompt.showVariableOptions = // This flag means that we should show variable/list options about scope
optVarType !== this.ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE &&
p.prompt.title !== this.ScratchBlocks.Msg.RENAME_VARIABLE_MODAL_TITLE &&
p.prompt.title !== this.ScratchBlocks.Msg.RENAME_LIST_MODAL_TITLE;
p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud;
this.setState(p);
}
handleConnectionModalStart (extensionId) {
@@ -395,11 +426,17 @@ class Blocks extends React.Component {
handleOpenSoundRecorder () {
this.props.onOpenSoundRecorder();
}
handlePromptCallback (input, optionSelection) {
/*
* Pass along information about proposed name and variable options (scope and isCloud)
* and additional potentially conflicting variable names from the VM
* to the variable validation prompt callback used in scratch-blocks.
*/
handlePromptCallback (input, variableOptions) {
this.state.prompt.callback(
input,
this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType),
optionSelection);
variableOptions);
this.handlePromptClose();
}
handlePromptClose () {
@@ -423,6 +460,7 @@ class Blocks extends React.Component {
/* eslint-disable no-unused-vars */
const {
anyModalVisible,
canUseCloud,
customProceduresVisible,
extensionLibraryVisible,
options,
@@ -450,11 +488,13 @@ class Blocks extends React.Component {
/>
{this.state.prompt ? (
<Prompt
defaultValue={this.state.prompt.defaultValue}
isStage={vm.runtime.getEditingTarget().isStage}
label={this.state.prompt.message}
placeholder={this.state.prompt.defaultValue}
showMoreOptions={this.state.prompt.showMoreOptions}
showCloudOption={this.state.prompt.showCloudOption}
showVariableOptions={this.state.prompt.showVariableOptions}
title={this.state.prompt.title}
vm={vm}
onCancel={this.handlePromptClose}
onOk={this.handlePromptCallback}
/>
@@ -481,6 +521,7 @@ class Blocks extends React.Component {
Blocks.propTypes = {
anyModalVisible: PropTypes.bool,
canUseCloud: PropTypes.bool,
customProceduresVisible: PropTypes.bool,
extensionLibraryVisible: PropTypes.bool,
isRtl: PropTypes.bool,
+6
View File
@@ -20,6 +20,9 @@ class Controls extends React.Component {
if (e.shiftKey) {
this.props.vm.setTurboMode(!this.props.turbo);
} else {
if (!this.props.isStarted) {
this.props.vm.start();
}
this.props.vm.greenFlag();
analytics.event({
category: 'general',
@@ -38,6 +41,7 @@ class Controls extends React.Component {
render () {
const {
vm, // eslint-disable-line no-unused-vars
isStarted, // eslint-disable-line no-unused-vars
projectRunning,
turbo,
...props
@@ -55,12 +59,14 @@ class Controls extends React.Component {
}
Controls.propTypes = {
isStarted: PropTypes.bool.isRequired,
projectRunning: PropTypes.bool.isRequired,
turbo: PropTypes.bool.isRequired,
vm: PropTypes.instanceOf(VM)
};
const mapStateToProps = state => ({
isStarted: state.scratchGui.vmStatus.running,
projectRunning: state.scratchGui.vmStatus.running,
turbo: state.scratchGui.vmStatus.turbo
});
+1 -1
View File
@@ -37,7 +37,7 @@ class CostumeLibrary extends React.PureComponent {
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
skinId: null
};
this.props.vm.addCostume(item.md5, vmCostume);
this.props.vm.addCostumeFromLibrary(item.md5, vmCostume);
analytics.event({
category: 'library',
action: 'Select Costume',
+7 -3
View File
@@ -150,8 +150,12 @@ class CostumeTab extends React.Component {
handleDuplicateCostume (costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex);
}
handleNewCostume (costume) {
this.props.vm.addCostume(costume.md5, costume);
handleNewCostume (costume, fromCostumeLibrary) {
if (fromCostumeLibrary) {
this.props.vm.addCostumeFromLibrary(costume.md5, costume);
} else {
this.props.vm.addCostume(costume.md5, costume);
}
}
handleNewBlankCostume () {
const name = this.props.vm.editingTarget.isStage ?
@@ -173,7 +177,7 @@ class CostumeTab extends React.Component {
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
skinId: null
};
this.handleNewCostume(vmCostume);
this.handleNewCostume(vmCostume, true /* fromCostumeLibrary */);
}
handleSurpriseBackdrop () {
const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
Loaded 100 of 249 files, more files were not shown because too many files have changed in this diff. Show more