move progress from status bar to firmware button

This commit is contained in:
David Lechner
2021-01-19 17:20:56 -06:00
parent 816053ba7c
commit bab35e8c45
12 changed files with 154 additions and 90 deletions
+53 -20
View File
@@ -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<FlashFirmwareActionType.Progress> & {
/**
* 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<FlashFirmwareActionType.DidStart>;
/**
* 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<FlashFirmwareActionType.DidProgress> & {
/** 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<FlashFirmwareActionType.DidFinish>;
/** 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<FlashFirmwareActionType.DidFailToFinish>;
/** 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;
+8 -3
View File
@@ -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<OpenFileButtonProps, 'enabled'>;
type StateProps = Pick<
OpenFileButtonProps,
'tooltip' | 'enabled' | 'showProgress' | 'progress'
>;
type DispatchProps = Pick<OpenFileButtonProps, 'onFile' | 'onReject' | 'onClick'>;
type OwnProps = Pick<OpenFileButtonProps, 'id'>;
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,
+27 -4
View File
@@ -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<Props> {
>
{({ getRootProps, getInputProps }): JSX.Element => (
<Tooltip
content={this.props.i18n.translate(this.props.tooltip)}
content={this.props.i18n.translate(
this.props.tooltip,
this.props.tooltip === TooltipId.FlashProgress
? {
percent:
this.props.progress === undefined
? ''
: this.props.i18n.formatPercentage(
this.props.progress,
),
}
: undefined,
)}
position={Position.BOTTOM}
hoverOpenDelay={tooltipDelay}
>
@@ -102,7 +118,14 @@ class OpenFileButton extends React.Component<Props> {
: {})}
>
<input {...getInputProps()} />
<img src={this.props.icon} alt={this.props.id} />
{this.props.showProgress ? (
<Spinner
value={this.props.progress}
intent={Intent.PRIMARY}
/>
) : (
<img src={this.props.icon} alt={this.props.id} />
)}
</Button>
</Tooltip>
)}
+7 -20
View File
@@ -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<StatusProps> {
class StatusBar extends React.Component {
render(): JSX.Element {
return (
<div className="status-bar" onContextMenu={(e): void => e.preventDefault()}>
<ProgressBar
className="status-bar-item"
value={this.props.progress}
animate={false}
/>
</div>
<div
className="status-bar"
onContextMenu={(e): void => e.preventDefault()}
></div>
);
}
}
const mapStateToProps = (state: RootState): StateProps => ({
progress: state.status.progress,
});
export default connect(mapStateToProps)(StatusBar);
export default connect()(StatusBar);
+4 -1
View File
@@ -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" }
}
+3 -2
View File
@@ -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',
-10
View File
@@ -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;
}
+38
View File
@@ -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<boolean, Action> = (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<number | null, Action> = (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 });
+4 -4
View File
@@ -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,
});
-22
View File
@@ -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<number, Action> = (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 });
+9 -3
View File
@@ -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 {
+1 -1
View File
@@ -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;