diff --git a/package.json b/package.json
index 37a9de0..6d00796 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"lodash.bindall": "^4.4.0",
"lodash.defaultsdeep": "^4.6.1",
"minilog": "^3.1.0",
+ "minimist": "^1.2.5",
"mkdirp": "^1.0.4",
"nets": "^3.2.0",
"react": "16.2.0",
diff --git a/src/main/argv.js b/src/main/argv.js
new file mode 100644
index 0000000..25be323
--- /dev/null
+++ b/src/main/argv.js
@@ -0,0 +1,23 @@
+import minimist from 'minimist';
+
+// inspired by yargs' process-argv
+export const isElectronApp = () => !!process.versions.electron;
+export const isElectronBundledApp = () => isElectronApp() && !process.defaultApp;
+
+export const parseAndTrimArgs = argv => {
+ // bundled Electron app: ignore 1 from "my-app arg1 arg2"
+ // unbundled Electron app: ignore 2 from "electron main/index.js arg1 arg2"
+ // node.js app: ignore 2 from "node src/index.js arg1 arg2"
+ const ignoreCount = isElectronBundledApp() ? 1 : 2;
+
+ const parsed = minimist(argv);
+
+ // ignore arguments AFTER parsing to handle cases like "electron --inspect=42 my.js arg1 arg2"
+ parsed._ = parsed._.slice(ignoreCount);
+
+ return parsed;
+};
+
+const argv = parseAndTrimArgs(process.argv);
+
+export default argv;
diff --git a/src/main/index.js b/src/main/index.js
index 9b0f04b..1688313 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -2,7 +2,9 @@ import {BrowserWindow, Menu, app, dialog, ipcMain, systemPreferences} from 'elec
import fs from 'fs-extra';
import path from 'path';
import {URL} from 'url';
+import {promisify} from 'util';
+import argv from './argv';
import {getFilterForExtension} from './FileFilters';
import telemetry from './ScratchDesktopTelemetry';
import MacOSMenu from './MacOSMenu';
@@ -374,3 +376,29 @@ app.on('ready', () => {
ipcMain.on('open-about-window', () => {
_windows.about.show();
});
+
+// start loading initial project data before the GUI needs it so the load seems faster
+const initialProjectDataPromise = (async () => {
+ if (argv._.length === 0) {
+ // no command line argument means no initial project data
+ return;
+ }
+ if (argv._.length > 1) {
+ log.warn(`Expected 1 command line argument but received ${argv._.length}.`);
+ }
+ const projectPath = argv._[argv._.length - 1];
+ try {
+ const projectData = await promisify(fs.readFile)(projectPath, null);
+ return projectData;
+ } catch (e) {
+ dialog.showMessageBox(_windows.main, {
+ type: 'error',
+ title: 'Failed to load project',
+ message: `Could not load project from file:\n${projectPath}`,
+ detail: e.message
+ });
+ }
+ // load failed: initial project data undefined
+})(); // IIFE
+
+ipcMain.handle('get-initial-project-data', () => initialProjectDataPromise);
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 49ff284..1fe1a37 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -1,16 +1,33 @@
-import {ipcRenderer, shell} from 'electron';
+import {ipcRenderer, remote, shell} from 'electron';
import bindAll from 'lodash.bindall';
+import omit from 'lodash.omit';
+import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
+import {connect} from 'react-redux';
import {compose} from 'redux';
-import GUI, {AppStateHOC} from 'scratch-gui';
+import GUI from 'scratch-gui/src/index';
+import VM from 'scratch-vm';
+
+import AppStateHOC from 'scratch-gui/src/lib/app-state-hoc.jsx';
+import {
+ LoadingStates,
+ onFetchedProjectData,
+ onLoadedProject,
+ defaultProjectId,
+ requestNewProject,
+ requestProjectUpload,
+ setProjectId
+} from 'scratch-gui/src/reducers/project-state';
+import {
+ openLoadingProject,
+ closeLoadingProject
+} from 'scratch-gui/src/reducers/modals';
import ElectronStorageHelper from '../common/ElectronStorageHelper';
import styles from './app.css';
-const defaultProjectId = 0;
-
// override window.open so that it uses the OS's default browser, not an electron browser
window.open = function (url, target) {
if (target === '_blank') {
@@ -38,9 +55,38 @@ const ScratchDesktopHOC = function (WrappedComponent) {
'handleTelemetryModalOptOut',
'handleUpdateProjectTitle'
]);
- this.state = {
- projectTitle: null
- };
+ this.props.onLoadingStarted();
+ ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => {
+ const hasInitialProject = initialProjectData && (initialProjectData.length > 0);
+ this.props.onHasInitialProject(hasInitialProject, this.props.loadingState);
+ if (!hasInitialProject) {
+ this.props.onLoadingCompleted();
+ return;
+ }
+ this.props.vm.loadProject(initialProjectData).then(
+ () => {
+ this.props.onLoadingCompleted();
+ this.props.onLoadedProject(this.props.loadingState, true);
+ },
+ e => {
+ this.props.onLoadingCompleted();
+ this.props.onLoadedProject(this.props.loadingState, false);
+ remote.dialog.showMessageBox(remote.getCurrentWindow(), {
+ type: 'error',
+ title: 'Failed to load project',
+ message: 'Invalid or corrupt project file.',
+ detail: e.message
+ });
+
+ // this effectively sets the default project ID
+ // TODO: maybe setting the default project ID should be implicit in `requestNewProject`
+ this.props.onHasInitialProject(false, this.props.loadingState);
+
+ // restart as if we didn't have an initial project to load
+ this.props.onRequestNewProject();
+ }
+ );
+ });
}
componentDidMount () {
ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave);
@@ -71,11 +117,13 @@ const ScratchDesktopHOC = function (WrappedComponent) {
}
render () {
const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
+
+ const childProps = omit(this.props, Object.keys(ScratchDesktopComponent.propTypes));
+
return ();
}
}
- return ScratchDesktopComponent;
+ ScratchDesktopComponent.propTypes = {
+ loadingState: PropTypes.oneOf(LoadingStates),
+ onFetchedInitialProjectData: PropTypes.func,
+ onHasInitialProject: PropTypes.func,
+ onLoadedProject: PropTypes.func,
+ onLoadingCompleted: PropTypes.func,
+ onLoadingStarted: PropTypes.func,
+ onRequestNewProject: PropTypes.func,
+ vm: PropTypes.instanceOf(VM).isRequired
+ };
+ const mapStateToProps = state => {
+ const loadingState = state.scratchGui.projectState.loadingState;
+ return {
+ loadingState: loadingState,
+ vm: state.scratchGui.vm
+ };
+ };
+ const mapDispatchToProps = dispatch => ({
+ onLoadingStarted: () => dispatch(openLoadingProject()),
+ onLoadingCompleted: () => dispatch(closeLoadingProject()),
+ onHasInitialProject: (hasInitialProject, loadingState) => {
+ if (hasInitialProject) {
+ // emulate sb-file-uploader
+ return dispatch(requestProjectUpload(loadingState));
+ }
+
+ // `createProject()` might seem more appropriate but it's not a valid state transition here
+ // setting the default project ID is a valid transition from NOT_LOADED and acts like "create new"
+ return dispatch(setProjectId(defaultProjectId));
+ },
+ onFetchedInitialProjectData: (projectData, loadingState) =>
+ dispatch(onFetchedProjectData(projectData, loadingState)),
+ onLoadedProject: (loadingState, loadSuccess) => {
+ const canSaveToServer = false;
+ return dispatch(onLoadedProject(loadingState, canSaveToServer, loadSuccess));
+ },
+ onRequestNewProject: () => dispatch(requestNewProject(false))
+ });
+
+ return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopComponent);
};
// note that redux's 'compose' function is just being used as a general utility to make
// the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's
// ability to compose reducers.
const WrappedGui = compose(
- ScratchDesktopHOC,
- AppStateHOC
+ AppStateHOC,
+ ScratchDesktopHOC
)(GUI);
ReactDOM.render(, appTarget);