add open and save button

Chrome has an annoying warning when downloading .py files, e.g https://stackoverflow.com/q/34130774/1976323

Workaround is to enable the "Ask where to save each file before downloading" setting.
This commit is contained in:
David Lechner
2020-05-22 19:54:48 -05:00
committed by David Lechner
parent ac6c798148
commit 1898787eb5
8 changed files with 185 additions and 7 deletions
+36
View File
@@ -6,6 +6,14 @@ export enum EditorActionType {
* The current (active) editor changed.
*/
Current = 'editor.action.current',
/**
* Save the current file to disk.
*/
Save = 'editor.action.save',
/**
* Open a file.
*/
Open = 'editor.action.open',
}
export interface CurrentEditorAction extends Action<EditorActionType.Current> {
@@ -21,3 +29,31 @@ export function setEditSession(
): CurrentEditorAction {
return { type: EditorActionType.Current, editSession };
}
/**
* Action that saves the current file.
*/
export type EditorSaveAction = Action<EditorActionType.Save>;
/**
* Creates an action to save the current file
*/
export function save(): EditorSaveAction {
return { type: EditorActionType.Save };
}
/**
* Action that opens a file.
*/
export interface EditorOpenAction extends Action<EditorActionType.Open> {
/** The data to save */
data: ArrayBuffer;
}
/**
* Creates an action to save a file
* @param data The file data
*/
export function open(data: ArrayBuffer): EditorOpenAction {
return { type: EditorActionType.Open, data };
}
+40
View File
@@ -0,0 +1,40 @@
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import * as editor from '../actions/editor';
import * as notification from '../actions/notification';
import { RootState } from '../reducers';
import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton';
type StateProps = Pick<OpenFileButtonProps, 'enabled'>;
type DispatchProps = Pick<OpenFileButtonProps, 'onFile' | 'onReject'>;
type OwnProps = Pick<OpenFileButtonProps, 'id'>;
const mapStateToProps = (state: RootState): StateProps => ({
enabled: state.editor.current !== null,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onFile: (data): void => {
dispatch(editor.open(data));
},
onReject: (file): void => {
dispatch(
notification.add('error', `'${file.name}' is not a valid python file.`),
);
},
});
const mergeProps = (
stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: OwnProps,
): OpenFileButtonProps => ({
fileExtension: '.py',
tooltip: 'Load file',
icon: 'open.svg',
...ownProps,
...stateProps,
...dispatchProps,
});
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(OpenFileButton);
+33
View File
@@ -0,0 +1,33 @@
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import * as editor from '../actions/editor';
import { RootState } from '../reducers';
import ActionButton, { ActionButtonProps } from './ActionButton';
type StateProps = Pick<ActionButtonProps, 'enabled' | 'context'>;
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
type OwnProps = Pick<ActionButtonProps, 'id'>;
const mapStateToProps = (state: RootState): StateProps => ({
enabled: state.editor.current !== null,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onAction: (): void => {
dispatch(editor.save());
},
});
const mergeProps = (
stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: OwnProps,
): ActionButtonProps => ({
tooltip: 'Save file',
icon: 'download.svg',
...ownProps,
...stateProps,
...dispatchProps,
});
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(ActionButton);
+6
View File
@@ -3,14 +3,20 @@ import ButtonGroup from 'react-bootstrap/ButtonGroup';
import ButtonToolbar from 'react-bootstrap/ButtonToolbar';
import BluetoothButton from './BluetoothButton';
import FlashButton from './FlashButton';
import LoadButton from './LoadButton';
import ReplButton from './ReplButton';
import RunButton from './RunButton';
import SaveButton from './SaveButton';
import StopButton from './StopButton';
class Toolbar extends React.Component {
render(): JSX.Element {
return (
<ButtonToolbar className="m-2">
<ButtonGroup className="mr-2" size="lg">
<LoadButton id="load" />
<SaveButton id="save" />
</ButtonGroup>
<ButtonGroup className="mr-2" size="lg">
<BluetoothButton id="bluetooth" />
<RunButton id="run" />
+44
View File
@@ -0,0 +1,44 @@
import * as FileSaver from 'file-saver';
import { Action, Dispatch } from 'redux';
import { EditorActionType, EditorOpenAction } from '../actions/editor';
import { RootState } from '../reducers';
import { combineServices } from '.';
const decoder = new TextDecoder();
async function open(
action: Action,
_dispatch: Dispatch,
state: RootState,
): Promise<void> {
if (action.type !== EditorActionType.Open) {
return;
}
// istanbul ignore next: currently, it is a bug if there is no current editor
if (state.editor.current === null) {
console.error('No current editor');
return;
}
const text = decoder.decode((action as EditorOpenAction).data);
state.editor.current.getDocument().setValue(text);
}
async function save(
action: Action,
_dispatch: Dispatch,
state: RootState,
): Promise<void> {
if (action.type !== EditorActionType.Save) {
return;
}
// istanbul ignore next: currently, it is a bug if there is no current editor
if (state.editor.current === null) {
console.error('No current editor');
return;
}
const data = state.editor.current.getDocument().getValue();
const blob = new Blob([data], { type: 'text/x-python;charset=utf-8' });
FileSaver.saveAs(blob, 'main.py');
}
export default combineServices(open, save);
+14 -7
View File
@@ -1,25 +1,32 @@
import { Action, Dispatch, Middleware } from 'redux';
import { RootState } from '../reducers';
import bootloader from './bootloader';
import editor from './editor';
type Service = (action: Action, dispatch: Dispatch) => Promise<void>;
type Service = (action: Action, dispatch: Dispatch, state: RootState) => Promise<void>;
function runService(service: Service, action: Action, dispatch: Dispatch): void {
service(action, dispatch).catch((err) =>
function runService(
service: Service,
action: Action,
dispatch: Dispatch,
state: RootState,
): void {
service(action, dispatch, state).catch((err) =>
console.log(`Unhandled exception in service: ${err}`),
);
}
export function combineServices(...services: Service[]): Service {
return (a, d): Promise<void> => {
services.forEach((s) => runService(s, a, d));
return (a, d, s): Promise<void> => {
services.forEach((x) => runService(x, a, d, s));
return Promise.resolve();
};
}
const rootService = combineServices(bootloader);
const rootService = combineServices(bootloader, editor);
const serviceMiddleware: Middleware = (store) => (next) => (action): unknown => {
runService(rootService, action, store.dispatch);
runService(rootService, action, store.dispatch, store.getState());
return next(action);
};