From bab35e8c454a6e0490314fdd91fbbd07b6378694 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 19 Jan 2021 12:09:22 -0600 Subject: [PATCH] move progress from status bar to firmware button --- src/actions/flash-firmware.ts | 73 ++++++++++++++++++++++-------- src/components/FlashButton.tsx | 11 +++-- src/components/OpenFileButton.tsx | 31 +++++++++++-- src/components/StatusBar.tsx | 27 +++-------- src/components/button-i18n.en.json | 5 +- src/components/button-i18n.ts | 5 +- src/components/status-bar.scss | 10 ---- src/reducers/firmware.ts | 38 ++++++++++++++++ src/reducers/index.ts | 8 ++-- src/reducers/status.ts | 22 --------- src/sagas/flash-firmware.ts | 12 +++-- src/variables.scss | 2 +- 12 files changed, 154 insertions(+), 90 deletions(-) create mode 100644 src/reducers/firmware.ts delete mode 100644 src/reducers/status.ts diff --git a/src/actions/flash-firmware.ts b/src/actions/flash-firmware.ts index c31b86cb..a55454f6 100644 --- a/src/actions/flash-firmware.ts +++ b/src/actions/flash-firmware.ts @@ -1,20 +1,23 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors import { Action } from 'redux'; +import { assert } from '../utils'; /** * High-level bootloader actions. */ export enum FlashFirmwareActionType { - /** - * Flash new firmware to the device. - */ + /** Request to flash new firmware to the device. */ FlashFirmware = 'flashFirmware.action.flashFirmware', - /** - * Firmware flash progress. - */ - Progress = 'flashFirmware.action.progress', + /** Flashing started. */ + DidStart = 'flashFirmware.action.didStart', + /** Firmware flash progress. */ + DidProgress = 'flashFirmware.action.didProgress', + /** Flashing finished successfully. */ + DidFinish = 'flashFirmware.action.didFinish', + /** Flashing firmware failed. */ + DidFailToFinish = 'flashFirmware.action.didFailToFinish', } /** @@ -33,19 +36,46 @@ export function flashFirmware(data?: ArrayBuffer): FlashFirmwareFlashAction { return { type: FlashFirmwareActionType.FlashFirmware, data }; } -export type FlashFirmwareProgressAction = Action & { - /** - * The number of bytes that have been flashed so far. - */ - complete: number; - /** - * The total number of bytes to be flashed. - */ - total: number; +/** Action that indicates flashing firmware started. */ +export type FlashFirmwareDidStartAction = Action; + +/** + * Action that indicates flashing firmware started. + * @param total The total number of bytes to be flashed. + */ +export function didStart(): FlashFirmwareDidStartAction { + return { type: FlashFirmwareActionType.DidStart }; +} + +/** Action that indicates current firmware flashing progress. */ +export type FlashFirmwareDidProgressAction = Action & { + /** The current progress (0 to 1). */ + value: number; }; -export function progress(complete: number, total: number): FlashFirmwareProgressAction { - return { type: FlashFirmwareActionType.Progress, complete, total }; +/** + * Action that indicates current firmware flashing progress. + * @param value The current progress (0 to 1). + */ +export function didProgress(value: number): FlashFirmwareDidProgressAction { + assert(value >= 0 && value <= 1, 'value out of range'); + return { type: FlashFirmwareActionType.DidProgress, value }; +} + +/** Action that indicates that flashing firmware completed successfully. */ +export type FlashFirmwareDidFinishAction = Action; + +/** Action that indicates that flashing firmware completed successfully. */ +export function didFinish(): FlashFirmwareDidFinishAction { + return { type: FlashFirmwareActionType.DidFinish }; +} + +/** Action that indicates that flashing failed. */ +export type FlashFirmwareDidFailToFinishAction = Action; + +/** Action that indicates that flashing failed. */ +export function didFailToFinish(): FlashFirmwareDidFailToFinishAction { + return { type: FlashFirmwareActionType.DidFailToFinish }; } /** @@ -53,4 +83,7 @@ export function progress(complete: number, total: number): FlashFirmwareProgress */ export type FlashFirmwareAction = | FlashFirmwareFlashAction - | FlashFirmwareProgressAction; + | FlashFirmwareDidStartAction + | FlashFirmwareDidProgressAction + | FlashFirmwareDidFinishAction + | FlashFirmwareDidFailToFinishAction; diff --git a/src/components/FlashButton.tsx b/src/components/FlashButton.tsx index fdfafc96..86d60e28 100644 --- a/src/components/FlashButton.tsx +++ b/src/components/FlashButton.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 { Dispatch } from '../actions'; @@ -11,12 +11,18 @@ import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton'; import { TooltipId } from './button-i18n'; import firmwareIcon from './images/firmware.svg'; -type StateProps = Pick; +type StateProps = Pick< + OpenFileButtonProps, + 'tooltip' | 'enabled' | 'showProgress' | 'progress' +>; type DispatchProps = Pick; type OwnProps = Pick; const mapStateToProps = (state: RootState): StateProps => ({ + tooltip: state.firmware.flashing ? TooltipId.FlashProgress : TooltipId.Flash, enabled: state.bootloader.connection === BootloaderConnectionState.Disconnected, + showProgress: state.firmware.flashing, + progress: state.firmware.progress === null ? undefined : state.firmware.progress, }); const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ @@ -39,7 +45,6 @@ const mergeProps = ( ownProps: OwnProps, ): OpenFileButtonProps => ({ fileExtension: '.zip', - tooltip: TooltipId.Flash, icon: firmwareIcon, ...ownProps, ...stateProps, diff --git a/src/components/OpenFileButton.tsx b/src/components/OpenFileButton.tsx index 5b15d74f..e63c07e7 100644 --- a/src/components/OpenFileButton.tsx +++ b/src/components/OpenFileButton.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors -import { Button, Intent, Position, Tooltip } from '@blueprintjs/core'; +import { Button, Intent, Position, Spinner, Tooltip } from '@blueprintjs/core'; import { WithI18nProps, withI18n } from '@shopify/react-i18n'; import React from 'react'; import Dropzone, { FileRejection } from 'react-dropzone'; @@ -20,6 +20,10 @@ export interface OpenFileButtonProps { readonly icon: string; /** When true or undefined, the button is enabled. */ readonly enabled?: boolean; + /** Show progress spinner instead of icon. */ + readonly showProgress?: boolean; + /** The progress value (0 to 1) for the progress spinner. */ + readonly progress?: number; /** Callback that is called when a file has been selected and opened for reading. */ readonly onFile: (data: ArrayBuffer) => void; /** Callback that is called when a file has been rejected (e.g. bad file extension). */ @@ -80,7 +84,19 @@ class OpenFileButton extends React.Component { > {({ getRootProps, getInputProps }): JSX.Element => ( @@ -102,7 +118,14 @@ class OpenFileButton extends React.Component { : {})} > - {this.props.id} + {this.props.showProgress ? ( + + ) : ( + {this.props.id} + )} )} diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index aa330b5a..e49b305e 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,33 +1,20 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors -import { ProgressBar } from '@blueprintjs/core'; import React from 'react'; import { connect } from 'react-redux'; -import { RootState } from '../reducers'; import './status-bar.scss'; -type StateProps = { progress: number }; - -type StatusProps = StateProps; - -class StatusBar extends React.Component { +class StatusBar extends React.Component { render(): JSX.Element { return ( -
e.preventDefault()}> - -
+
e.preventDefault()} + >
); } } -const mapStateToProps = (state: RootState): StateProps => ({ - progress: state.status.progress, -}); - -export default connect(mapStateToProps)(StatusBar); +export default connect()(StatusBar); diff --git a/src/components/button-i18n.en.json b/src/components/button-i18n.en.json index fbeee51d..ec1f2a8d 100644 --- a/src/components/button-i18n.en.json +++ b/src/components/button-i18n.en.json @@ -8,6 +8,9 @@ "connect": { "tooltip": "Connect using Bluetooth" }, "disconnect": { "tooltip": "Disconnect Bluetooth" } }, - "flash": { "tooltip": "Install Pybricks firmware" }, + "flash": { + "action": { "tooltip": "Install Pybricks firmware" }, + "progress": { "tooltip": "Flashing… {percent}" } + }, "settings": { "tooltip": "Settings" } } diff --git a/src/components/button-i18n.ts b/src/components/button-i18n.ts index b34b8aac..2fafaadf 100644 --- a/src/components/button-i18n.ts +++ b/src/components/button-i18n.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors // File: components/button-i18n.ts // Button translation keys. @@ -9,7 +9,8 @@ export enum TooltipId { Run = 'run.tooltip', Stop = 'stop.tooltip', Repl = 'repl.tooltip', - Flash = 'flash.tooltip', + Flash = 'flash.action.tooltip', + FlashProgress = 'flash.progress.tooltip', BluetoothConnect = 'bluetooth.connect.tooltip', BluetoothDisconnect = 'bluetooth.disconnect.tooltip', Settings = 'settings.tooltip', diff --git a/src/components/status-bar.scss b/src/components/status-bar.scss index ea0b6469..3ac18ad6 100644 --- a/src/components/status-bar.scss +++ b/src/components/status-bar.scss @@ -14,13 +14,3 @@ display: flex; align-items: center; } - -.status-bar-item { - width: 25%; - margin-left: 10px; -} - -.#{$ns}-progress-bar.status-bar-item { - // override progress bar default gray1 backgound - background-color: $pt-app-background-color; -} diff --git a/src/reducers/firmware.ts b/src/reducers/firmware.ts new file mode 100644 index 00000000..fee94835 --- /dev/null +++ b/src/reducers/firmware.ts @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { Reducer, combineReducers } from 'redux'; +import { Action } from '../actions'; +import { FlashFirmwareActionType } from '../actions/flash-firmware'; + +export interface FirmwareState { + /** The firmware is being erased/flashed right now. */ + flashing: boolean; + /** The current progress (0 to 1) or null for unknown (e.g erasing) */ + progress: number | null; +} + +const flashing: Reducer = (state = false, action) => { + switch (action.type) { + case FlashFirmwareActionType.DidStart: + return true; + case FlashFirmwareActionType.DidFinish: + case FlashFirmwareActionType.DidFailToFinish: + return false; + default: + return state; + } +}; + +const progress: Reducer = (state = null, action) => { + switch (action.type) { + case FlashFirmwareActionType.DidStart: + return null; + case FlashFirmwareActionType.DidProgress: + return action.value; + default: + return state; + } +}; + +export default combineReducers({ flashing, progress }); diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 25c65740..46aa9798 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors import { combineReducers } from 'redux'; import app, { AppState } from './app'; import ble, { BleState } from './ble'; import bootloader, { BootloaderState } from './bootloader'; import editor, { EditorState } from './editor'; +import firmware, { FirmwareState } from './firmware'; import hub, { HubState } from './hub'; import license, { LicenseState } from './license'; import settings, { SettingsState } from './settings'; -import status, { StatusState } from './status'; import terminal, { TerminalState } from './terminal'; /** @@ -20,10 +20,10 @@ export interface RootState { readonly bootloader: BootloaderState; readonly ble: BleState; readonly editor: EditorState; + readonly firmware: FirmwareState; readonly hub: HubState; readonly license: LicenseState; readonly settings: SettingsState; - readonly status: StatusState; readonly terminal: TerminalState; } @@ -32,9 +32,9 @@ export default combineReducers({ bootloader, ble, editor, + firmware, hub, license, settings, - status, terminal, }); diff --git a/src/reducers/status.ts b/src/reducers/status.ts deleted file mode 100644 index d882116c..00000000 --- a/src/reducers/status.ts +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors - -import { Reducer } from 'react'; -import { combineReducers } from 'redux'; -import { Action } from '../actions'; -import { FlashFirmwareActionType } from '../actions/flash-firmware'; - -const progress: Reducer = (state = -1, action) => { - switch (action.type) { - case FlashFirmwareActionType.Progress: - return action.complete / action.total; - default: - return state; - } -}; - -export interface StatusState { - readonly progress: number; -} - -export default combineReducers({ progress }); diff --git a/src/sagas/flash-firmware.ts b/src/sagas/flash-firmware.ts index 7ef58344..bb095420 100644 --- a/src/sagas/flash-firmware.ts +++ b/src/sagas/flash-firmware.ts @@ -21,7 +21,9 @@ import { Action } from '../actions'; import { FlashFirmwareActionType, FlashFirmwareFlashAction, - progress, + didFinish, + didProgress, + didStart, } from '../actions/flash-firmware'; import { BootloaderChecksumRequestAction, @@ -265,6 +267,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { } } + yield put(didStart()); + const eraseAction = (yield put(eraseRequest())) as BootloaderEraseRequestAction; const [, erase] = (yield all([ waitForDidSend(eraseAction.id), @@ -301,7 +305,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { )) as BootloaderProgramRequestAction; yield waitForDidSend(programAction.id); - yield put(progress(offset, firmware.length)); + yield put(didProgress(offset / firmware.length)); // we don't want to request checksum if this is the last packet since // the bootloader will send a response to the program request already. @@ -344,11 +348,13 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { throw Error("Didn't flash all bytes"); } - yield put(progress(firmware.length, firmware.length)); + yield put(didProgress(1)); // this will cause the remote device to disconnect and reboot const rebootAction = (yield put(rebootRequest())) as BootloaderRebootRequestAction; yield waitForDidSend(rebootAction.id); + + yield put(didFinish()); } export default function* (): Generator { diff --git a/src/variables.scss b/src/variables.scss index 17189444..80abb973 100644 --- a/src/variables.scss +++ b/src/variables.scss @@ -11,7 +11,7 @@ $pt-font-size-large: $pt-grid-size * 1.8; $pt-font-size-small: $pt-grid-size * 1.4; $pt-navbar-height: 72px; -$pb-status-bar-height: 3vh; +$pb-status-bar-height: 24px; $pb-pybricks-blue: #0088ce; $pt-app-background-color: #e8e8e8;