From 25243b0542dd6f13c3ae87814f3b988a9206f840 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Thu, 3 Sep 2020 16:11:01 -0700
Subject: [PATCH 1/5] npm i --save-dev minimist
---
package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/package.json b/package.json
index c2f3c92..8e0d831 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",
From e2f39580dfe2c9c961f9d93511ef9b851623edf6 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Thu, 3 Sep 2020 17:01:22 -0700
Subject: [PATCH 2/5] make command line args available to render process
---
src/main/argv.js | 23 +++++++++++++++++++++++
src/main/index.js | 3 +++
src/renderer/app.jsx | 10 ++++++++++
3 files changed, 36 insertions(+)
create mode 100644 src/main/argv.js
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 229d4a4..c806962 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -3,6 +3,7 @@ import fs from 'fs-extra';
import path from 'path';
import {URL} from 'url';
+import argv from './argv';
import {getFilterForExtension} from './FileFilters';
import telemetry from './ScratchDesktopTelemetry';
import MacOSMenu from './MacOSMenu';
@@ -373,3 +374,5 @@ app.on('ready', () => {
ipcMain.on('open-about-window', () => {
_windows.about.show();
});
+
+ipcMain.handle('get-argv', () => argv);
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 49ff284..e3c1169 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -6,6 +6,7 @@ import {compose} from 'redux';
import GUI, {AppStateHOC} from 'scratch-gui';
import ElectronStorageHelper from '../common/ElectronStorageHelper';
+import log from '../common/log';
import styles from './app.css';
@@ -99,4 +100,13 @@ const WrappedGui = compose(
AppStateHOC
)(GUI);
+ipcRenderer.invoke('get-argv').then(
+ argv => {
+ log.log(`argv._ = ${argv._}`);
+ },
+ err => {
+ log.warn('Failed to retrieve argv', err);
+ }
+);
+
ReactDOM.render(, appTarget);
From 05e8b26a34d1a749b174660c6b9afef3cd5c6de6 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Fri, 4 Sep 2020 13:26:34 -0700
Subject: [PATCH 3/5] WIP: actually load project from command line
Doing it this way works for the initial load but overrides later actions
like File -> New.
---
src/main/index.js | 27 ++++++++++++++++++++++++++-
src/renderer/app.jsx | 31 ++++++++++++++++++++-----------
2 files changed, 46 insertions(+), 12 deletions(-)
diff --git a/src/main/index.js b/src/main/index.js
index c806962..1545521 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -2,6 +2,7 @@ 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';
@@ -375,4 +376,28 @@ ipcMain.on('open-about-window', () => {
_windows.about.show();
});
-ipcMain.handle('get-argv', () => argv);
+// 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 e3c1169..9ad78c0 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -6,7 +6,6 @@ import {compose} from 'redux';
import GUI, {AppStateHOC} from 'scratch-gui';
import ElectronStorageHelper from '../common/ElectronStorageHelper';
-import log from '../common/log';
import styles from './app.css';
@@ -40,8 +39,16 @@ const ScratchDesktopHOC = function (WrappedComponent) {
'handleUpdateProjectTitle'
]);
this.state = {
- projectTitle: null
+ projectTitle: null,
+ projectLoading: true
};
+
+ ipcRenderer.invoke('get-initial-project-data').then(projectData => {
+ this.setState({
+ projectData,
+ projectLoading: false
+ });
+ });
}
componentDidMount () {
ipcRenderer.on('setTitleFromSave', this.handleSetTitleFromSave);
@@ -72,6 +79,11 @@ const ScratchDesktopHOC = function (WrappedComponent) {
}
render () {
const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
+
+ if (this.state.projectLoading) {
+ return
Loading File...
;
+ }
+
return ();
}
@@ -100,13 +118,4 @@ const WrappedGui = compose(
AppStateHOC
)(GUI);
-ipcRenderer.invoke('get-argv').then(
- argv => {
- log.log(`argv._ = ${argv._}`);
- },
- err => {
- log.warn('Failed to retrieve argv', err);
- }
-);
-
ReactDOM.render(, appTarget);
From a9933242e0daa417fd9ccb2492b33f83f77d5954 Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Tue, 15 Sep 2020 14:32:36 -0700
Subject: [PATCH 4/5] use project state system to load initial project
---
src/renderer/app.jsx | 112 +++++++++++++++++++++++++++++++++----------
1 file changed, 87 insertions(+), 25 deletions(-)
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 9ad78c0..0e73fff 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -1,16 +1,33 @@
import {ipcRenderer, 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,16 +55,29 @@ const ScratchDesktopHOC = function (WrappedComponent) {
'handleTelemetryModalOptOut',
'handleUpdateProjectTitle'
]);
- this.state = {
- projectTitle: null,
- projectLoading: true
- };
+ 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();
+ console.error(e); // TODO: dialog box reporting the error?
- ipcRenderer.invoke('get-initial-project-data').then(projectData => {
- this.setState({
- projectData,
- projectLoading: false
- });
+ // abandon this load and instead fetch+load the default project
+ // WARNING: this stuff doesn't currently work correctly!
+ this.props.onLoadedProject(this.props.loadingState, false);
+ this.props.onRequestNewProject();
+ }
+ );
});
}
componentDidMount () {
@@ -80,15 +110,12 @@ const ScratchDesktopHOC = function (WrappedComponent) {
render () {
const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
- if (this.state.projectLoading) {
- return Loading File...
;
- }
+ 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);
From 0fa4c4b2ed88e6e3532ecc3914ecb2e1492af8ab Mon Sep 17 00:00:00 2001
From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com>
Date: Tue, 15 Sep 2020 14:52:08 -0700
Subject: [PATCH 5/5] handle initial project load errors
---
src/renderer/app.jsx | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx
index 0e73fff..1fe1a37 100644
--- a/src/renderer/app.jsx
+++ b/src/renderer/app.jsx
@@ -1,4 +1,4 @@
-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';
@@ -70,11 +70,19 @@ const ScratchDesktopHOC = function (WrappedComponent) {
},
e => {
this.props.onLoadingCompleted();
- console.error(e); // TODO: dialog box reporting the error?
-
- // abandon this load and instead fetch+load the default project
- // WARNING: this stuff doesn't currently work correctly!
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();
}
);