From 520e11c0cf630a3b43368a209e86a75e448a9b18 Mon Sep 17 00:00:00 2001
From: Ray Schamp <ray@scratch.mit.edu>
Date: Thu, 4 May 2017 17:41:35 -0400
Subject: [PATCH] Add Save and Load buttons

Use the de/serialization features of the VM to allow downloading and loading project JSON
---
 src/components/button/button.css           |  3 ++
 src/components/button/button.jsx           | 30 +++++++++++
 src/components/load-button/load-button.css |  3 ++
 src/components/load-button/load-button.jsx | 36 +++++++++++++
 src/components/menu-bar/menu-bar.jsx       |  6 ++-
 src/containers/load-button.jsx             | 55 +++++++++++++++++++
 src/containers/save-button.jsx             | 62 ++++++++++++++++++++++
 7 files changed, 194 insertions(+), 1 deletion(-)
 create mode 100644 src/components/button/button.css
 create mode 100644 src/components/button/button.jsx
 create mode 100644 src/components/load-button/load-button.css
 create mode 100644 src/components/load-button/load-button.jsx
 create mode 100644 src/containers/load-button.jsx
 create mode 100644 src/containers/save-button.jsx

diff --git a/src/components/button/button.css b/src/components/button/button.css
new file mode 100644
index 000000000..44243650f
--- /dev/null
+++ b/src/components/button/button.css
@@ -0,0 +1,3 @@
+.button {
+    cursor: pointer;
+}
diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx
new file mode 100644
index 000000000..34b485154
--- /dev/null
+++ b/src/components/button/button.jsx
@@ -0,0 +1,30 @@
+const classNames = require('classnames');
+const PropTypes = require('prop-types');
+const React = require('react');
+
+const styles = require('./button.css');
+
+const ButtonComponent = ({
+    className,
+    onClick,
+    children,
+    ...props
+}) => (
+    <span
+        className={classNames(
+            styles.button,
+            className
+        )}
+        onClick={onClick}
+        {...props}
+    >
+        {children}
+    </span>
+);
+
+ButtonComponent.propTypes = {
+    children: PropTypes.node,
+    className: PropTypes.string,
+    onClick: PropTypes.func.isRequired
+};
+module.exports = ButtonComponent;
diff --git a/src/components/load-button/load-button.css b/src/components/load-button/load-button.css
new file mode 100644
index 000000000..527718028
--- /dev/null
+++ b/src/components/load-button/load-button.css
@@ -0,0 +1,3 @@
+.file-input {
+    display: none;
+}
diff --git a/src/components/load-button/load-button.jsx b/src/components/load-button/load-button.jsx
new file mode 100644
index 000000000..29b2962e5
--- /dev/null
+++ b/src/components/load-button/load-button.jsx
@@ -0,0 +1,36 @@
+const PropTypes = require('prop-types');
+const React = require('react');
+
+const ButtonComponent = require('../button/button.jsx');
+
+const styles = require('./load-button.css');
+
+const LoadButtonComponent = ({
+    inputRef,
+    onChange,
+    onClick,
+    title,
+    ...props
+}) => (
+    <span {...props}>
+        <ButtonComponent onClick={onClick}>{title}</ButtonComponent>
+        <input
+            className={styles.fileInput}
+            ref={inputRef}
+            type="file"
+            onChange={onChange}
+        />
+    </span>
+);
+
+LoadButtonComponent.propTypes = {
+    className: PropTypes.string,
+    inputRef: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onClick: PropTypes.func.isRequired,
+    title: PropTypes.string
+};
+LoadButtonComponent.defaultProps = {
+    title: 'Load'
+};
+module.exports = LoadButtonComponent;
diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index b34a2e190..4397b5002 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -2,6 +2,9 @@ const classNames = require('classnames');
 const React = require('react');
 
 const Box = require('../box/box.jsx');
+const LoadButton = require('../../containers/load-button.jsx');
+const SaveButton = require('../../containers/save-button.jsx');
+
 const styles = require('./menu-bar.css');
 const scratchLogo = require('./scratch-logo.svg');
 
@@ -18,7 +21,8 @@ const MenuBar = function MenuBar () {
                     src={scratchLogo}
                 />
             </div>
-            <div className={styles.menuItem} >Animation Playtest Prototype</div>
+            <SaveButton className={styles.menuItem} />
+            <LoadButton className={styles.menuItem} />
         </Box>
     );
 };
diff --git a/src/containers/load-button.jsx b/src/containers/load-button.jsx
new file mode 100644
index 000000000..dcbd4fe01
--- /dev/null
+++ b/src/containers/load-button.jsx
@@ -0,0 +1,55 @@
+const bindAll = require('lodash.bindall');
+const PropTypes = require('prop-types');
+const React = require('react');
+const {connect} = require('react-redux');
+
+const LoadButtonComponent = require('../components/load-button/load-button.jsx');
+
+class LoadButton extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'setFileInput',
+            'handleChange',
+            'handleClick'
+        ]);
+    }
+    handleChange (e) {
+        const reader = new FileReader();
+        reader.onload = () => this.props.loadProject(reader.result);
+        reader.readAsText(e.target.files[0]);
+    }
+    handleClick () {
+        this.fileInput.click();
+    }
+    setFileInput (input) {
+        this.fileInput = input;
+    }
+    render () {
+        const {
+            loadProject, // eslint-disable-line no-unused-vars
+            ...props
+        } = this.props;
+        return (
+            <LoadButtonComponent
+                inputRef={this.setFileInput}
+                onChange={this.handleChange}
+                onClick={this.handleClick}
+                {...props}
+            />
+        );
+    }
+}
+
+LoadButton.propTypes = {
+    loadProject: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+    loadProject: state.vm.fromJSON.bind(state.vm)
+});
+
+module.exports = connect(
+    mapStateToProps,
+    () => ({}) // omit dispatch prop
+)(LoadButton);
diff --git a/src/containers/save-button.jsx b/src/containers/save-button.jsx
new file mode 100644
index 000000000..f5e7ab018
--- /dev/null
+++ b/src/containers/save-button.jsx
@@ -0,0 +1,62 @@
+const bindAll = require('lodash.bindall');
+const PropTypes = require('prop-types');
+const React = require('react');
+const {connect} = require('react-redux');
+
+const ButtonComponent = require('../components/button/button.jsx');
+
+class SaveButton extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleClick'
+        ]);
+    }
+    handleClick () {
+        const json = this.props.saveProjectSb3();
+
+        // Download project data into a file - create link element,
+        // simulate click on it, and then remove it.
+        const saveLink = document.createElement('a');
+        document.body.appendChild(saveLink);
+
+        const data = new Blob([json], {type: 'text'});
+        const url = window.URL.createObjectURL(data);
+        saveLink.href = url;
+
+        // File name: project-DATE-TIME
+        const date = new Date();
+        const timestamp = `${date.toLocaleDateString()}-${date.toLocaleTimeString()}`;
+        saveLink.download = `project-${timestamp}.json`;
+        saveLink.click();
+        window.URL.revokeObjectURL(url);
+        document.body.removeChild(saveLink);
+    }
+    render () {
+        const {
+            saveProjectSb3, // eslint-disable-line no-unused-vars
+            ...props
+        } = this.props;
+        return (
+            <ButtonComponent
+                onClick={this.handleClick}
+                {...props}
+            >
+                Save
+            </ButtonComponent>
+        );
+    }
+}
+
+SaveButton.propTypes = {
+    saveProjectSb3: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+    saveProjectSb3: state.vm.saveProjectSb3.bind(state.vm)
+});
+
+module.exports = connect(
+    mapStateToProps,
+    () => ({}) // omit dispatch prop
+)(SaveButton);
-- 
GitLab