mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 17:45:22 +00:00
Add progress bar for user program download.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { RootState } from '../reducers';
|
||||
@@ -9,7 +9,7 @@ import { downloadAndRun } from './actions';
|
||||
import { HubRuntimeState } from './reducers';
|
||||
import runIcon from './run.svg';
|
||||
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled'>;
|
||||
type StateProps = Pick<ActionButtonProps, 'enabled' | 'showProgress' | 'progress'>;
|
||||
type DispatchProps = Pick<ActionButtonProps, 'onAction'>;
|
||||
type OwnProps = Pick<ActionButtonProps, 'id'> &
|
||||
Pick<ActionButtonProps, 'keyboardShortcut'>;
|
||||
@@ -17,6 +17,9 @@ type OwnProps = Pick<ActionButtonProps, 'id'> &
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
enabled:
|
||||
state.editor.current !== null && state.hub.runtime === HubRuntimeState.Idle,
|
||||
showProgress: state.hub.runtime === HubRuntimeState.Loading,
|
||||
progress:
|
||||
state.hub.downloadProgress === null ? undefined : state.hub.downloadProgress,
|
||||
});
|
||||
|
||||
const mapDispatchToProps: DispatchProps = {
|
||||
@@ -29,6 +32,7 @@ const mergeProps = (
|
||||
ownProps: OwnProps,
|
||||
): ActionButtonProps => ({
|
||||
tooltip: TooltipId.Run,
|
||||
progressTooltip: TooltipId.RunProgress,
|
||||
icon: runIcon,
|
||||
...ownProps,
|
||||
...stateProps,
|
||||
|
||||
@@ -32,6 +32,7 @@ export type HubMessageAction = HubChecksumMessageAction;
|
||||
export enum HubActionType {
|
||||
DownloadAndRun = 'hub.action.downloadAndRun',
|
||||
DidStartDownload = 'hub.action.didStartDownload',
|
||||
DidProgressDownload = 'hub.action.didProgressDownload',
|
||||
DidFinishDownload = 'hub.action.didFinishDownload',
|
||||
DidFailToFinishDownload = 'hub.action.didFailToFinishDownload',
|
||||
Stop = 'hub.action.stop',
|
||||
@@ -50,6 +51,14 @@ export function didStartDownload(): HubDidStartDownloadAction {
|
||||
return { type: HubActionType.DidStartDownload };
|
||||
}
|
||||
|
||||
export type HubDidProgressDownloadAction = Action<HubActionType.DidProgressDownload> & {
|
||||
progress: number;
|
||||
};
|
||||
|
||||
export function didProgressDownload(progress: number): HubDidProgressDownloadAction {
|
||||
return { type: HubActionType.DidProgressDownload, progress };
|
||||
}
|
||||
|
||||
export type HubDidFinishDownloadAction = Action<HubActionType.DidFinishDownload>;
|
||||
|
||||
export function didFinishDownload(): HubDidFinishDownloadAction {
|
||||
@@ -80,6 +89,7 @@ export function repl(): HubReplAction {
|
||||
export type HubAction =
|
||||
| HubDownloadAndRunAction
|
||||
| HubDidStartDownloadAction
|
||||
| HubDidProgressDownloadAction
|
||||
| HubDidFinishDownloadAction
|
||||
| HubDidFailToFinishDownloadAction
|
||||
| HubStopAction
|
||||
|
||||
+16
-1
@@ -66,4 +66,19 @@ const runtime: Reducer<HubRuntimeState, Action> = (
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({ runtime });
|
||||
const downloadProgress: Reducer<number | null, Action> = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case HubActionType.DidStartDownload:
|
||||
return 0;
|
||||
case HubActionType.DidProgressDownload:
|
||||
return action.progress;
|
||||
case HubActionType.DidFinishDownload:
|
||||
return 1;
|
||||
case HubActionType.DidFailToFinishDownload:
|
||||
return null;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({ runtime, downloadProgress });
|
||||
|
||||
+20
-1
@@ -12,7 +12,14 @@ import {
|
||||
import { BleUartActionType, BleUartWriteAction, didWrite } from '../ble-uart/actions';
|
||||
import { MpyActionType, didCompile } from '../mpy/actions';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import { HubActionType, checksum, downloadAndRun, repl, stop } from './actions';
|
||||
import {
|
||||
HubActionType,
|
||||
HubDidProgressDownloadAction,
|
||||
checksum,
|
||||
downloadAndRun,
|
||||
repl,
|
||||
stop,
|
||||
} from './actions';
|
||||
import hub from './sagas';
|
||||
|
||||
jest.mock('ace-builds');
|
||||
@@ -44,6 +51,11 @@ describe('downloadAndRun', () => {
|
||||
saga.put(didWrite((writeAction as BleUartWriteAction).id));
|
||||
saga.put(checksum(30));
|
||||
|
||||
// then progress is updated
|
||||
const progressAction = await saga.take();
|
||||
expect(progressAction.type).toBe(HubActionType.DidProgressDownload);
|
||||
expect((progressAction as HubDidProgressDownloadAction).progress).toBe(0);
|
||||
|
||||
// then the first chunk of 20 bytes
|
||||
const writeAction2 = await saga.take();
|
||||
expect(writeAction2.type).toBe(BleUartActionType.Write);
|
||||
@@ -51,6 +63,13 @@ describe('downloadAndRun', () => {
|
||||
saga.put(didWrite((writeAction2 as BleUartWriteAction).id));
|
||||
saga.put(checksum(0));
|
||||
|
||||
// then progress is updated
|
||||
const progress2Action = await saga.take();
|
||||
expect(progress2Action.type).toBe(HubActionType.DidProgressDownload);
|
||||
expect((progress2Action as HubDidProgressDownloadAction).progress).toBe(
|
||||
20 / 30,
|
||||
);
|
||||
|
||||
// then last chunk
|
||||
const writeAction3 = await saga.take();
|
||||
expect(writeAction3.type).toBe(BleUartActionType.Write);
|
||||
|
||||
+5
-1
@@ -45,6 +45,7 @@ import {
|
||||
HubStopAction,
|
||||
didFailToFinishDownload as didFailToFinishDownload,
|
||||
didFinishDownload,
|
||||
didProgressDownload,
|
||||
didStartDownload,
|
||||
} from './actions';
|
||||
|
||||
@@ -91,6 +92,9 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
// let everyone know the runtime is busy loading the program
|
||||
yield* put(didStartDownload());
|
||||
|
||||
// TODO: show compiled size in UI?
|
||||
console.log(`Downloading ${mpy.data.byteLength} bytes`);
|
||||
|
||||
const checksumChannel = yield* actionChannel<HubChecksumMessageAction>(
|
||||
HubMessageActionType.Checksum,
|
||||
);
|
||||
@@ -138,6 +142,7 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
|
||||
// we can actually only write 20 bytes at a time
|
||||
for (let j = 0; j < chunk.length; j += SafeTxCharLength) {
|
||||
yield* put(didProgressDownload((i + j) / mpy.data.byteLength));
|
||||
const writeAction = yield* put(
|
||||
write(nextMessageId(), chunk.slice(j, j + SafeTxCharLength)),
|
||||
);
|
||||
@@ -147,7 +152,6 @@ function* downloadAndRun(_action: HubDownloadAndRunAction): Generator {
|
||||
yield* put(didFailToFinishDownload());
|
||||
return;
|
||||
}
|
||||
// TODO: dispatch progress
|
||||
}
|
||||
|
||||
const { checksumAction, checksumTimeout } = yield* race({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020 The Pybricks Authors
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
HotkeysTarget,
|
||||
Intent,
|
||||
Position,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
} from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
@@ -23,10 +24,16 @@ export interface ActionButtonProps {
|
||||
readonly keyboardShortcut?: string;
|
||||
/** Tooltip text that appears when hovering over the button. */
|
||||
readonly tooltip: TooltipId;
|
||||
/** Tooltip text that appears when hovering over the button and @showProgress is true. */
|
||||
readonly progressTooltip?: TooltipId;
|
||||
/** Icon shown on the button. */
|
||||
readonly icon: string;
|
||||
/** When true or undefined, the button is enabled. */
|
||||
readonly enabled?: boolean;
|
||||
/** When true, show progress indicator instead of icon. */
|
||||
readonly showProgress?: boolean;
|
||||
/** The progress value (0 to 1) or undefined for indeterminate progress. */
|
||||
readonly progress?: number;
|
||||
/** Callback that is called when the button is activated (clicked). */
|
||||
readonly onAction: () => void;
|
||||
}
|
||||
@@ -38,10 +45,28 @@ class ActionButton extends React.Component<Props> {
|
||||
private buttonRef: React.RefObject<Button> = React.createRef();
|
||||
|
||||
render(): JSX.Element {
|
||||
let tooltipText = this.props.i18n.translate(this.props.tooltip);
|
||||
if (this.props.keyboardShortcut) {
|
||||
tooltipText += ` (${this.props.keyboardShortcut})`;
|
||||
}
|
||||
const {
|
||||
i18n,
|
||||
id,
|
||||
icon,
|
||||
keyboardShortcut,
|
||||
enabled,
|
||||
tooltip,
|
||||
progressTooltip,
|
||||
showProgress,
|
||||
progress,
|
||||
onAction,
|
||||
} = this.props;
|
||||
|
||||
const tooltipText =
|
||||
showProgress && progressTooltip
|
||||
? i18n.translate(progressTooltip, {
|
||||
percent:
|
||||
progress === undefined ? '' : i18n.formatPercentage(progress),
|
||||
})
|
||||
: i18n.translate(tooltip) +
|
||||
(keyboardShortcut ? ` (${keyboardShortcut})` : '');
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tooltipText}
|
||||
@@ -52,16 +77,16 @@ class ActionButton extends React.Component<Props> {
|
||||
ref={this.buttonRef}
|
||||
intent={Intent.PRIMARY}
|
||||
onMouseDown={(e) => e.preventDefault()} // prevent focus
|
||||
onClick={(): void => this.props.onAction()}
|
||||
disabled={this.props.enabled === false}
|
||||
onClick={(): void => onAction()}
|
||||
disabled={enabled === false}
|
||||
className="no-box-shadow"
|
||||
style={
|
||||
this.props.enabled === false
|
||||
? { pointerEvents: 'none' }
|
||||
: undefined
|
||||
}
|
||||
style={enabled === false ? { pointerEvents: 'none' } : undefined}
|
||||
>
|
||||
<img src={this.props.icon} alt={this.props.id} />
|
||||
{showProgress ? (
|
||||
<Spinner value={progress} intent={Intent.PRIMARY} />
|
||||
) : (
|
||||
<img src={icon} alt={id} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"open": { "tooltip": "Open file" },
|
||||
"saveAs": { "tooltip": "Download file" },
|
||||
"stop": { "tooltip": "Stop everything" },
|
||||
"run": { "tooltip": "Download and run this program" },
|
||||
"run": {
|
||||
"action": { "tooltip": "Download and run this program" },
|
||||
"progress": { "tooltip": "Downloading… {percent}" }
|
||||
},
|
||||
"repl": { "tooltip": "Start REPL in terminal" },
|
||||
"bluetooth": {
|
||||
"connect": { "tooltip": "Connect using Bluetooth" },
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
export enum TooltipId {
|
||||
Open = 'open.tooltip',
|
||||
SaveAs = 'saveAs.tooltip',
|
||||
Run = 'run.tooltip',
|
||||
Run = 'run.action.tooltip',
|
||||
RunProgress = 'run.progress.tooltip',
|
||||
Stop = 'stop.tooltip',
|
||||
Repl = 'repl.tooltip',
|
||||
Flash = 'flash.action.tooltip',
|
||||
|
||||
Reference in New Issue
Block a user