From e8c4472e50a5841a412efdcd80e896abd315cb36 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 12 Apr 2021 16:24:36 -0500 Subject: [PATCH] Add progress bar for user program download. --- src/hub/RunButton.tsx | 8 ++++-- src/hub/actions.ts | 10 +++++++ src/hub/reducers.ts | 17 +++++++++++- src/hub/sagas.test.ts | 21 ++++++++++++++- src/hub/sagas.ts | 6 ++++- src/toolbar/ActionButton.tsx | 51 +++++++++++++++++++++++++++--------- src/toolbar/i18n.en.json | 5 +++- src/toolbar/i18n.ts | 3 ++- 8 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/hub/RunButton.tsx b/src/hub/RunButton.tsx index a26fbdfa..a19d12d1 100644 --- a/src/hub/RunButton.tsx +++ b/src/hub/RunButton.tsx @@ -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; +type StateProps = Pick; type DispatchProps = Pick; type OwnProps = Pick & Pick; @@ -17,6 +17,9 @@ type OwnProps = Pick & 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, diff --git a/src/hub/actions.ts b/src/hub/actions.ts index b67dd48b..5cea0641 100644 --- a/src/hub/actions.ts +++ b/src/hub/actions.ts @@ -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 & { + progress: number; +}; + +export function didProgressDownload(progress: number): HubDidProgressDownloadAction { + return { type: HubActionType.DidProgressDownload, progress }; +} + export type HubDidFinishDownloadAction = Action; export function didFinishDownload(): HubDidFinishDownloadAction { @@ -80,6 +89,7 @@ export function repl(): HubReplAction { export type HubAction = | HubDownloadAndRunAction | HubDidStartDownloadAction + | HubDidProgressDownloadAction | HubDidFinishDownloadAction | HubDidFailToFinishDownloadAction | HubStopAction diff --git a/src/hub/reducers.ts b/src/hub/reducers.ts index 4628d17d..822456c8 100644 --- a/src/hub/reducers.ts +++ b/src/hub/reducers.ts @@ -66,4 +66,19 @@ const runtime: Reducer = ( } }; -export default combineReducers({ runtime }); +const downloadProgress: Reducer = (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 }); diff --git a/src/hub/sagas.test.ts b/src/hub/sagas.test.ts index 1e79333e..1d3cd2db 100644 --- a/src/hub/sagas.test.ts +++ b/src/hub/sagas.test.ts @@ -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); diff --git a/src/hub/sagas.ts b/src/hub/sagas.ts index cd795dcd..9b1ca0e5 100644 --- a/src/hub/sagas.ts +++ b/src/hub/sagas.ts @@ -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( 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({ diff --git a/src/toolbar/ActionButton.tsx b/src/toolbar/ActionButton.tsx index f0cafcfa..f84eb5d2 100644 --- a/src/toolbar/ActionButton.tsx +++ b/src/toolbar/ActionButton.tsx @@ -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 { private buttonRef: React.RefObject ); diff --git a/src/toolbar/i18n.en.json b/src/toolbar/i18n.en.json index ec1f2a8d..b820fd37 100644 --- a/src/toolbar/i18n.en.json +++ b/src/toolbar/i18n.en.json @@ -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" }, diff --git a/src/toolbar/i18n.ts b/src/toolbar/i18n.ts index 657adce1..2f1ce117 100644 --- a/src/toolbar/i18n.ts +++ b/src/toolbar/i18n.ts @@ -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',