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:
commit
11083c1c9d
249 files changed
+11756
-5476
No files matched your search
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# NPM
|
||||
/node_modules
|
||||
npm-*
|
||||
/package-lock.json
|
||||
|
||||
# Testing
|
||||
/.nyc_output
|
||||
|
||||
+10
@@ -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'
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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}));
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -0,0 +1,4 @@
|
||||
.alerts-inner-container {
|
||||
min-width: 200px;
|
||||
max-width: 548px;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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%);
|
||||
}
|
||||
@@ -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 : (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.75;
|
||||
background-color: #8cbcff;
|
||||
background-color: $drop-highlight;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -10,7 +10,7 @@
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
height: calc(100% - $library-header-height);
|
||||
height: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.save-now {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,10 @@
|
||||
@import "../../css/colors.css";
|
||||
|
||||
.share-button {
|
||||
background: $data-primary;
|
||||
}
|
||||
|
||||
.share-button-is-shared {
|
||||
background: $ui-black-transparent;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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),
|
||||
|
||||
File renamed without changes.
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
Reference in new issue
Block a user