From 98b3733fe7f2b32bfd0ae987f32a4377e7aff020 Mon Sep 17 00:00:00 2001
From: Karishma Chadha <kchadha@scratch.mit.edu>
Date: Wed, 20 Jun 2018 12:45:17 -0400
Subject: [PATCH] Allow exporting and importing .sprite3s

---
 src/components/asset-panel/selector.jsx       |  3 +++
 .../sprite-selector-item.jsx                  | 12 +++++++++-
 .../sprite-selector/sprite-list.jsx           |  3 +++
 .../sprite-selector/sprite-selector.jsx       |  5 +++-
 src/components/target-pane/target-pane.jsx    |  3 +++
 src/containers/sprite-selector-item.jsx       |  8 +++++++
 src/containers/target-pane.jsx                | 24 +++++++++++++++++++
 7 files changed, 56 insertions(+), 2 deletions(-)

diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx
index 88d609723..2462986d8 100644
--- a/src/components/asset-panel/selector.jsx
+++ b/src/components/asset-panel/selector.jsx
@@ -24,6 +24,7 @@ const Selector = props => {
         onRemoveSortable,
         onDeleteClick,
         onDuplicateClick,
+        onExportClick,
         onItemClick
     } = props;
 
@@ -76,6 +77,7 @@ const Selector = props => {
                             onClick={onItemClick}
                             onDeleteButtonClick={onDeleteClick}
                             onDuplicateButtonClick={onDuplicateClick}
+                            onExportButtonClick={onExportClick}
                         />
                     </SortableAsset>
                 ))}
@@ -102,6 +104,7 @@ Selector.propTypes = {
     onAddSortable: PropTypes.func,
     onDeleteClick: PropTypes.func,
     onDuplicateClick: PropTypes.func,
+    onExportClick: PropTypes.func,
     onItemClick: PropTypes.func.isRequired,
     onRemoveSortable: PropTypes.func,
     ordering: PropTypes.arrayOf(PropTypes.number),
diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx
index 62364ec53..cb04c034a 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.jsx
+++ b/src/components/sprite-selector-item/sprite-selector-item.jsx
@@ -51,7 +51,7 @@ const SpriteSelectorItem = props => (
                 <div className={styles.spriteDetails}>{props.details}</div>
             ) : null}
         </div>
-        {props.onDuplicateButtonClick || props.onDeleteButtonClick ? (
+        {props.onDuplicateButtonClick || props.onDeleteButtonClick || props.onExportButtonClick ? (
             <ContextMenu id={`${props.name}-${contextMenuId++}`}>
                 {props.onDuplicateButtonClick ? (
                     <MenuItem onClick={props.onDuplicateButtonClick}>
@@ -71,6 +71,15 @@ const SpriteSelectorItem = props => (
                         />
                     </MenuItem>
                 ) : null }
+                {props.onExportButtonClick ? (
+                    <MenuItem onClick={props.onExportButtonClick}>
+                        <FormattedMessage
+                            defaultMessage="export"
+                            description="Menu item to export the selected item"
+                            id="gui.spriteSelectorItem.contextMenuExport"
+                        />
+                    </MenuItem>
+                ) : null }
             </ContextMenu>
         ) : null}
     </ContextMenuTrigger>
@@ -86,6 +95,7 @@ SpriteSelectorItem.propTypes = {
     onClick: PropTypes.func,
     onDeleteButtonClick: PropTypes.func,
     onDuplicateButtonClick: PropTypes.func,
+    onExportButtonClick: PropTypes.func,
     onMouseDown: PropTypes.func,
     onMouseEnter: PropTypes.func,
     onMouseLeave: PropTypes.func,
diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx
index 6195b0c9c..e22b5c8e0 100644
--- a/src/components/sprite-selector/sprite-list.jsx
+++ b/src/components/sprite-selector/sprite-list.jsx
@@ -20,6 +20,7 @@ const SpriteList = function (props) {
         hoveredTarget,
         onDeleteSprite,
         onDuplicateSprite,
+        onExportSprite,
         onSelectSprite,
         onAddSortable,
         onRemoveSortable,
@@ -79,6 +80,7 @@ const SpriteList = function (props) {
                             onClick={onSelectSprite}
                             onDeleteButtonClick={onDeleteSprite}
                             onDuplicateButtonClick={onDuplicateSprite}
+                            onExportButtonClick={onExportSprite}
                         />
                     </SortableAsset>
                 );
@@ -110,6 +112,7 @@ SpriteList.propTypes = {
     onAddSortable: PropTypes.func,
     onDeleteSprite: PropTypes.func,
     onDuplicateSprite: PropTypes.func,
+    onExportSprite: PropTypes.func,
     onRemoveSortable: PropTypes.func,
     onSelectSprite: PropTypes.func,
     ordering: PropTypes.arrayOf(PropTypes.number),
diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx
index 9907dffce..689a023ed 100644
--- a/src/components/sprite-selector/sprite-selector.jsx
+++ b/src/components/sprite-selector/sprite-selector.jsx
@@ -53,6 +53,7 @@ const SpriteSelectorComponent = function (props) {
         onDrop,
         onDeleteSprite,
         onDuplicateSprite,
+        onExportSprite,
         onFileUploadClick,
         onNewSpriteClick,
         onPaintSpriteClick,
@@ -105,6 +106,7 @@ const SpriteSelectorComponent = function (props) {
                     onDeleteSprite={onDeleteSprite}
                     onDrop={onDrop}
                     onDuplicateSprite={onDuplicateSprite}
+                    onExportSprite={onExportSprite}
                     onSelectSprite={onSelectSprite}
                 />
             </Box>
@@ -116,7 +118,7 @@ const SpriteSelectorComponent = function (props) {
                         title: intl.formatMessage(messages.addSpriteFromFile),
                         img: fileUploadIcon,
                         onClick: onFileUploadClick,
-                        fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2', // TODO add sprite 3
+                        fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2, .sprite3',
                         fileChange: onSpriteUpload,
                         fileInput: spriteFileInput
                     }, {
@@ -152,6 +154,7 @@ SpriteSelectorComponent.propTypes = {
     onDeleteSprite: PropTypes.func,
     onDrop: PropTypes.func,
     onDuplicateSprite: PropTypes.func,
+    onExportSprite: PropTypes.func,
     onFileUploadClick: PropTypes.func,
     onNewSpriteClick: PropTypes.func,
     onPaintSpriteClick: PropTypes.func,
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index 57b10e0c9..c620c8504 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -30,6 +30,7 @@ const TargetPane = ({
     onDeleteSprite,
     onDrop,
     onDuplicateSprite,
+    onExportSprite,
     onFileUploadClick,
     onNewSpriteClick,
     onPaintSpriteClick,
@@ -66,6 +67,7 @@ const TargetPane = ({
             onDeleteSprite={onDeleteSprite}
             onDrop={onDrop}
             onDuplicateSprite={onDuplicateSprite}
+            onExportSprite={onExportSprite}
             onFileUploadClick={onFileUploadClick}
             onNewSpriteClick={onNewSpriteClick}
             onPaintSpriteClick={onPaintSpriteClick}
@@ -133,6 +135,7 @@ TargetPane.propTypes = {
     onDeleteSprite: PropTypes.func,
     onDrop: PropTypes.func,
     onDuplicateSprite: PropTypes.func,
+    onExportSprite: PropTypes.func,
     onFileUploadClick: PropTypes.func,
     onNewSpriteClick: PropTypes.func,
     onPaintSpriteClick: PropTypes.func,
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index 4c968f827..785f9d08f 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -18,6 +18,7 @@ class SpriteSelectorItem extends React.Component {
             'handleClick',
             'handleDelete',
             'handleDuplicate',
+            'handleExport',
             'handleMouseEnter',
             'handleMouseLeave',
             'handleMouseDown',
@@ -84,6 +85,10 @@ class SpriteSelectorItem extends React.Component {
         e.stopPropagation(); // To prevent from bubbling back to handleClick
         this.props.onDuplicateButtonClick(this.props.id);
     }
+    handleExport (e) {
+        e.stopPropagation();
+        this.props.onExportButtonClick(this.props.id);
+    }
     handleMouseLeave () {
         this.props.dispatchSetHoveredSprite(null);
     }
@@ -99,6 +104,7 @@ class SpriteSelectorItem extends React.Component {
             onClick,
             onDeleteButtonClick,
             onDuplicateButtonClick,
+            onExportButtonClick,
             dragPayload,
             receivedBlocks,
             /* eslint-enable no-unused-vars */
@@ -109,6 +115,7 @@ class SpriteSelectorItem extends React.Component {
                 onClick={this.handleClick}
                 onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null}
                 onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
+                onExportButtonClick={onExportButtonClick ? this.handleExport : null}
                 onMouseDown={this.handleMouseDown}
                 onMouseEnter={this.handleMouseEnter}
                 onMouseLeave={this.handleMouseLeave}
@@ -134,6 +141,7 @@ SpriteSelectorItem.propTypes = {
     onDeleteButtonClick: PropTypes.func,
     onDrag: PropTypes.func.isRequired,
     onDuplicateButtonClick: PropTypes.func,
+    onExportButtonClick: PropTypes.func,
     receivedBlocks: PropTypes.bool.isRequired,
     selected: PropTypes.bool
 };
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index e3c7688dd..1d637330a 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -29,6 +29,7 @@ class TargetPane extends React.Component {
             'handleDeleteSprite',
             'handleDrop',
             'handleDuplicateSprite',
+            'handleExportSprite',
             'handleNewSprite',
             'handleSelectSprite',
             'handleSurpriseSpriteClick',
@@ -68,6 +69,28 @@ class TargetPane extends React.Component {
     handleDuplicateSprite (id) {
         this.props.vm.duplicateSprite(id);
     }
+    handleExportSprite (id) {
+        const spriteName = this.props.vm.runtime.getTargetById(id).getName();
+        const saveLink = document.createElement('a');
+        document.body.appendChild(saveLink);
+
+        this.props.vm.exportSprite(id).then(content => {
+            const filename = `${spriteName}.sprite3`;
+
+            // Use special ms version if available to get it working on Edge.
+            if (navigator.msSaveOrOpenBlob) {
+                navigator.msSaveOrOpenBlob(content, filename);
+                return;
+            }
+
+            const url = window.URL.createObjectURL(content);
+            saveLink.href = url;
+            saveLink.download = filename;
+            saveLink.click();
+            window.URL.revokeObjectURL(url);
+            document.body.removeChild(saveLink);
+        });
+    }
     handleSelectSprite (id) {
         this.props.vm.setEditingTarget(id);
     }
@@ -145,6 +168,7 @@ class TargetPane extends React.Component {
                 onDeleteSprite={this.handleDeleteSprite}
                 onDrop={this.handleDrop}
                 onDuplicateSprite={this.handleDuplicateSprite}
+                onExportSprite={this.handleExportSprite}
                 onFileUploadClick={this.handleFileUploadClick}
                 onPaintSpriteClick={this.handlePaintSpriteClick}
                 onSelectSprite={this.handleSelectSprite}
-- 
GitLab