@@ -165,39 +195,19 @@ class MenuBar extends React.Component {
/>
- {/* @TODO: remove coming soon tooltip wrapper https://github.com/LLK/scratch-gui/issues/2664 */}
-
+
+

+

+
+
- {deletedItem === 'Sprite' ?
- :
-
- }
+ {this.restoreOptionMessage(deletedItem)}
)}
diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css
index ae523e18801..911271f1997 100644
--- a/src/components/prompt/prompt.css
+++ b/src/components/prompt/prompt.css
@@ -88,8 +88,15 @@
.more-options-icon {
width: .75rem;
height: .75rem;
- margin-left: .5rem;
vertical-align: middle;
padding-bottom: .2rem;
opacity: .5;
}
+
+[dir="ltr"] .more-options-icon {
+ margin-left: .5rem;
+}
+
+[dir="rtl"] .more-options-icon {
+ margin-right: .5rem;
+}
diff --git a/src/components/stage-header/stage-header.css b/src/components/stage-header/stage-header.css
index dd2dc2c65ca..3e6f59c75ac 100644
--- a/src/components/stage-header/stage-header.css
+++ b/src/components/stage-header/stage-header.css
@@ -59,6 +59,10 @@
height: 100%;
}
+[dir="rtl"] .stage-button-icon {
+ transform: scaleX(-1);
+}
+
[dir="ltr"] .stage-button-first {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
diff --git a/src/components/target-pane/target-pane.css b/src/components/target-pane/target-pane.css
index 404283d19fd..07b07cb161a 100644
--- a/src/components/target-pane/target-pane.css
+++ b/src/components/target-pane/target-pane.css
@@ -11,5 +11,12 @@
display: flex;
flex-basis: 72px;
flex-shrink: 0;
+}
+
+[dir="ltr"] .stage-selector-wrapper {
margin-left: calc($space / 2);
}
+
+[dir="rtl"] .stage-selector-wrapper {
+ margin-right: calc($space / 2);
+}
diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx
index 7cc52870fea..2869a047a6b 100644
--- a/src/containers/controls.jsx
+++ b/src/containers/controls.jsx
@@ -64,5 +64,7 @@ const mapStateToProps = state => ({
projectRunning: state.scratchGui.vmStatus.running,
turbo: state.scratchGui.vmStatus.turbo
});
+// no-op function to prevent dispatch prop being passed to component
+const mapDispatchToProps = () => ({});
-export default connect(mapStateToProps)(Controls);
+export default connect(mapStateToProps, mapDispatchToProps)(Controls);
diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx
index 454c5ffcef7..4c63a8cceb7 100644
--- a/src/containers/costume-tab.jsx
+++ b/src/containers/costume-tab.jsx
@@ -24,6 +24,8 @@ import {
SOUNDS_TAB_INDEX
} from '../reducers/editor-tab';
+import {setRestore} from '../reducers/restore-deletion';
+
import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg';
import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg';
import fileUploadIcon from '../components/action-menu/icon--file-upload.svg';
@@ -135,7 +137,11 @@ class CostumeTab extends React.Component {
this.setState({selectedCostumeIndex: costumeIndex});
}
handleDeleteCostume (costumeIndex) {
- this.props.vm.deleteCostume(costumeIndex);
+ const restoreCostumeFun = this.props.vm.deleteCostume(costumeIndex);
+ this.props.dispatchUpdateRestore({
+ restoreFun: restoreCostumeFun,
+ deletedItem: 'Costume'
+ });
}
handleDuplicateCostume (costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex);
@@ -232,6 +238,7 @@ class CostumeTab extends React.Component {
}
render () {
const {
+ dispatchUpdateRestore, // eslint-disable-line no-unused-vars
intl,
onNewCostumeFromCameraClick,
onNewLibraryBackdropClick,
@@ -325,6 +332,7 @@ class CostumeTab extends React.Component {
CostumeTab.propTypes = {
cameraModalVisible: PropTypes.bool,
+ dispatchUpdateRestore: PropTypes.func,
editingTarget: PropTypes.string,
intl: intlShape,
onActivateSoundsTab: PropTypes.func.isRequired,
@@ -372,6 +380,9 @@ const mapDispatchToProps = dispatch => ({
},
onRequestCloseCameraModal: () => {
dispatch(closeCameraCapture());
+ },
+ dispatchUpdateRestore: restoreState => {
+ dispatch(setRestore(restoreState));
}
});
diff --git a/src/containers/error-boundary.jsx b/src/containers/error-boundary.jsx
index fbb15f3b998..6485a41b84b 100644
--- a/src/containers/error-boundary.jsx
+++ b/src/containers/error-boundary.jsx
@@ -78,6 +78,6 @@ const mapStateToProps = state => ({
});
// no-op function to prevent dispatch prop being passed to component
-const mapDispatchToProps = () => {};
+const mapDispatchToProps = () => ({});
export default connect(mapStateToProps, mapDispatchToProps)(ErrorBoundary);
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index cdcb87c8985..33d8fe58b24 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -94,6 +94,7 @@ class GUI extends React.Component {
}
GUI.propTypes = {
+ assetHost: PropTypes.string,
children: PropTypes.node,
fetchingProject: PropTypes.bool,
importInfoVisible: PropTypes.bool,
@@ -101,6 +102,7 @@ GUI.propTypes = {
onSeeCommunity: PropTypes.func,
previewInfoVisible: PropTypes.bool,
projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
+ projectHost: PropTypes.string,
vm: PropTypes.instanceOf(VM)
};
diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx
index 390f6ef598b..dc14578bd61 100644
--- a/src/containers/paint-editor-wrapper.jsx
+++ b/src/containers/paint-editor-wrapper.jsx
@@ -56,6 +56,7 @@ PaintEditorWrapper.propTypes = {
name: PropTypes.string,
rotationCenterX: PropTypes.number,
rotationCenterY: PropTypes.number,
+ rtl: PropTypes.bool,
selectedCostumeIndex: PropTypes.number.isRequired,
vm: PropTypes.instanceOf(VM)
};
@@ -74,6 +75,7 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => {
imageFormat: costume && costume.dataFormat,
imageId: targetId && `${targetId}${costume.skinId}`,
image: state.scratchGui.vm.getCostume(index),
+ rtl: state.locales.isRtl,
vm: state.scratchGui.vm
};
};
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 2245f3d2f6a..a30b8f97e82 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -34,6 +34,8 @@ import {
COSTUMES_TAB_INDEX
} from '../reducers/editor-tab';
+import {setRestore} from '../reducers/restore-deletion';
+
class SoundTab extends React.Component {
constructor (props) {
super(props);
@@ -76,10 +78,11 @@ class SoundTab extends React.Component {
}
handleDeleteSound (soundIndex) {
- this.props.vm.deleteSound(soundIndex);
+ const restoreFun = this.props.vm.deleteSound(soundIndex);
if (soundIndex >= this.state.selectedSoundIndex) {
this.setState({selectedSoundIndex: Math.max(0, soundIndex - 1)});
}
+ this.props.dispatchUpdateRestore({restoreFun, deletedItem: 'Sound'});
}
handleDuplicateSound (soundIndex) {
@@ -153,6 +156,7 @@ class SoundTab extends React.Component {
render () {
const {
+ dispatchUpdateRestore, // eslint-disable-line no-unused-vars
intl,
vm,
onNewSoundFromLibraryClick,
@@ -252,6 +256,7 @@ class SoundTab extends React.Component {
}
SoundTab.propTypes = {
+ dispatchUpdateRestore: PropTypes.func,
editingTarget: PropTypes.string,
intl: intlShape,
onActivateCostumesTab: PropTypes.func.isRequired,
@@ -294,6 +299,9 @@ const mapDispatchToProps = dispatch => ({
},
onRequestCloseSoundLibrary: () => {
dispatch(closeSoundLibrary());
+ },
+ dispatchUpdateRestore: restoreState => {
+ dispatch(setRestore(restoreState));
}
});
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index a0197ad6dd9..01e617c8e77 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -2,28 +2,24 @@ import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
-import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {setHoveredSprite} from '../reducers/hovered-target';
import {updateAssetDrag} from '../reducers/asset-drag';
import {getEventXY} from '../lib/touch-utils';
+import VM from 'scratch-vm';
+import {SVGRenderer} from 'scratch-svg-renderer';
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
const dragThreshold = 3; // Same as the block drag threshold
-
-const messages = defineMessages({
- deleteSpriteConfirmation: {
- defaultMessage: 'Are you sure you want to delete this?',
- description: 'Confirmation for deleting sprites',
- id: 'gui.spriteSelectorItem.deleteSpriteConfirmation'
- }
-});
+// Contains 'font-family', but doesn't only contain 'font-family="none"'
+const HAS_FONT_REGEXP = 'font-family(?!="none")';
class SpriteSelectorItem extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
+ 'getCostumeUrl',
'handleClick',
'handleDelete',
'handleDuplicate',
@@ -34,6 +30,34 @@ class SpriteSelectorItem extends React.Component {
'handleMouseMove',
'handleMouseUp'
]);
+ this.svgRenderer = new SVGRenderer();
+ // Asset ID of the SVG currently in SVGRenderer
+ this.svgRendererAssetId = null;
+ }
+ getCostumeUrl () {
+ if (this.props.costumeURL) return this.props.costumeURL;
+ if (!this.props.assetId) return null;
+
+ const storage = this.props.vm.runtime.storage;
+ const asset = storage.get(this.props.assetId);
+ // If the SVG refers to fonts, they must be inlined in order to display correctly in the img tag.
+ // Avoid parsing the SVG when possible, since it's expensive.
+ if (asset.assetType === storage.AssetType.ImageVector) {
+ // If the asset ID has not changed, no need to re-parse
+ if (this.svgRendererAssetId === this.props.assetId) {
+ return this.cachedUrl;
+ }
+
+ const svgString = this.props.vm.runtime.storage.get(this.props.assetId).decodeText();
+ if (svgString.match(HAS_FONT_REGEXP)) {
+ this.svgRendererAssetId = this.props.assetId;
+ this.svgRenderer.loadString(svgString);
+ const svgText = this.svgRenderer.toString(true /* shouldInjectFonts */);
+ this.cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
+ return this.cachedUrl;
+ }
+ }
+ return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI();
}
handleMouseUp () {
this.initialOffset = null;
@@ -58,7 +82,7 @@ class SpriteSelectorItem extends React.Component {
const dy = currentOffset.y - this.initialOffset.y;
if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) {
this.props.onDrag({
- img: this.props.costumeURL,
+ img: this.getCostumeUrl(),
currentOffset: currentOffset,
dragging: true,
dragType: this.props.dragType,
@@ -84,10 +108,7 @@ class SpriteSelectorItem extends React.Component {
}
handleDelete (e) {
e.stopPropagation(); // To prevent from bubbling back to handleClick
- // eslint-disable-next-line no-alert
- if (window.confirm(this.props.intl.formatMessage(messages.deleteSpriteConfirmation))) {
- this.props.onDeleteButtonClick(this.props.id);
- }
+ this.props.onDeleteButtonClick(this.props.id);
}
handleDuplicate (e) {
e.stopPropagation(); // To prevent from bubbling back to handleClick
@@ -115,11 +136,14 @@ class SpriteSelectorItem extends React.Component {
onExportButtonClick,
dragPayload,
receivedBlocks,
+ costumeURL,
+ vm,
/* eslint-enable no-unused-vars */
...props
} = this.props;
return (
({
- costumeURL: costumeURL || (assetId && state.scratchGui.vm.runtime.storage.get(assetId).encodeDataURI()),
+const mapStateToProps = (state, {id}) => ({
dragging: state.scratchGui.assetDrag.dragging,
receivedBlocks: state.scratchGui.hoveredTarget.receivedBlocks &&
- state.scratchGui.hoveredTarget.sprite === id
+ state.scratchGui.hoveredTarget.sprite === id,
+ vm: state.scratchGui.vm
});
const mapDispatchToProps = dispatch => ({
dispatchSetHoveredSprite: spriteId => {
@@ -168,8 +192,12 @@ const mapDispatchToProps = dispatch => ({
onDrag: data => dispatch(updateAssetDrag(data))
});
-
-export default connect(
+const ConnectedComponent = connect(
mapStateToProps,
mapDispatchToProps
-)(injectIntl(SpriteSelectorItem));
+)(SpriteSelectorItem);
+
+export {
+ ConnectedComponent as default,
+ HAS_FONT_REGEXP // Exposed for testing
+};
diff --git a/src/css/units.css b/src/css/units.css
index 59d9a2e7cdd..db51ed00934 100644
--- a/src/css/units.css
+++ b/src/css/units.css
@@ -6,6 +6,7 @@ $space: 0.5rem;
$sprites-per-row: 5;
$menu-bar-height: 3rem;
+$language-selector-width: 3rem;
$sprite-info-height: 6rem;
$stage-menu-height: 2.75rem;
diff --git a/src/lib/libraries/extensions/index.jsx b/src/lib/libraries/extensions/index.jsx
index 0bd3b36ee9a..edd4fa512b6 100644
--- a/src/lib/libraries/extensions/index.jsx
+++ b/src/lib/libraries/extensions/index.jsx
@@ -158,7 +158,7 @@ export default [
/>
),
featured: true,
- disabled: true,
+ disabled: false,
launchDeviceConnectionFlow: true,
useAutoScan: true,
deviceImage: wedoDeviceImage,
diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx
index b2256b82a4a..67e70f90019 100644
--- a/src/lib/vm-listener-hoc.jsx
+++ b/src/lib/vm-listener-hoc.jsx
@@ -89,6 +89,10 @@ const vmListenerHOC = function (WrappedComponent) {
onKeyUp,
onMonitorsUpdate,
onTargetsUpdate,
+ onProjectRunStart,
+ onProjectRunStop,
+ onTurboModeOff,
+ onTurboModeOn,
/* eslint-enable no-unused-vars */
...props
} = this.props;
diff --git a/test/integration/localization.test.js b/test/integration/localization.test.js
index abd8ee4676d..cf02aa86b1b 100644
--- a/test/integration/localization.test.js
+++ b/test/integration/localization.test.js
@@ -26,7 +26,6 @@ describe('Localization', () => {
await loadUri(uri);
await clickXpath('//button[@title="Try It"]');
await clickXpath('//*[@aria-label="language selector"]');
- await clickText('English');
await clickText('Deutsch');
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh
diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js
index 7c306a5eee2..ca1a3d275d8 100644
--- a/test/integration/sounds.test.js
+++ b/test/integration/sounds.test.js
@@ -33,8 +33,6 @@ describe('Working with sounds', () => {
// Delete the sound
await rightClickText('Meow', scope.soundsTab);
await clickText('delete', scope.soundsTab);
- await driver.switchTo().alert()
- .accept();
// Add it back
await clickXpath('//button[@aria-label="Choose a Sound"]');
diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js
index 41f2eec3ab4..5b99efcb62e 100644
--- a/test/integration/sprites.test.js
+++ b/test/integration/sprites.test.js
@@ -55,8 +55,6 @@ describe('Working with sprites', () => {
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await rightClickText('Sprite1', scope.spriteTile);
await clickText('delete', scope.spriteTile);
- await driver.switchTo().alert()
- .accept();
// Confirm that the stage has been switched to
await findByText('Stage selected: no motion blocks');
const logs = await getLogs();
diff --git a/test/unit/containers/sprite-selector-item.test.jsx b/test/unit/containers/sprite-selector-item.test.jsx
index 85032603d4b..25200a1fb5f 100644
--- a/test/unit/containers/sprite-selector-item.test.jsx
+++ b/test/unit/containers/sprite-selector-item.test.jsx
@@ -4,6 +4,7 @@ import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux';
import SpriteSelectorItem from '../../../src/containers/sprite-selector-item';
+import {HAS_FONT_REGEXP} from '../../../src/containers/sprite-selector-item';
import CloseButton from '../../../src/components/close-button/close-button';
describe('SpriteSelectorItem Container', () => {
@@ -48,22 +49,19 @@ describe('SpriteSelectorItem Container', () => {
onDeleteButtonClick = jest.fn();
dispatchSetHoveredSprite = jest.fn();
selected = true;
- // Mock window.confirm() which is called when the close button is clicked.
- global.confirm = jest.fn(() => true);
});
- test('should confirm if the user really wants to delete the sprite', () => {
+ test('should delete the sprite', () => {
const wrapper = mountWithIntl(getContainer());
wrapper.find(CloseButton).simulate('click');
- expect(global.confirm).toHaveBeenCalled();
expect(onDeleteButtonClick).toHaveBeenCalledWith(1337);
});
- test('should not delete the sprite if the user cancels', () => {
- global.confirm = jest.fn(() => false);
- const wrapper = mountWithIntl(getContainer());
- wrapper.find(CloseButton).simulate('click');
- expect(global.confirm).toHaveBeenCalled();
- expect(onDeleteButtonClick).not.toHaveBeenCalled();
+ test('Has font regexp works', () => {
+ expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
+ expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
+ expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy();
+
+ expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy();
});
});
diff --git a/webpack.config.js b/webpack.config.js
index a44d90c48e2..1013bb87a08 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -177,7 +177,7 @@ module.exports = [
])
})
].concat(
- process.env.NODE_ENV === 'production' ? (
+ process.env.NODE_ENV === 'production' || process.env.BUILD_MODE === 'dist' ? (
// export as library
defaultsDeep({}, base, {
target: 'web',