Merge pull request #700 from pybricks/dlech

v1.2.0-beta.1
This commit is contained in:
David Lechner
2021-12-27 18:31:42 -06:00
committed by GitHub
32 changed files with 282 additions and 147 deletions
+5 -2
View File
@@ -2,10 +2,11 @@
# Changelog
## [Unreleased]
## [1.2.0-beta.1] - 2021-12-27
### Added
- Status bar indicator for connected hub.
- Basic battery OK/low indicator ([support#559]).
### Changed
- Saving file now uses proper save dialog in Chromium browser ([support#84]).
@@ -17,6 +18,7 @@
[support#84]: https://github.com/pybricks/support/issues/84
[support#300]: https://github.com/pybricks/support/issues/300
[support#369]: https://github.com/pybricks/support/issues/369
[support#559]: https://github.com/pybricks/support/issues/559
## [1.1.0] - 2021-12-16
@@ -235,7 +237,8 @@ Prerelease changes are documented at [support#48].
<!-- links for version headings -->
[Unreleased]: https://github.com/pybricks/pybricks-code/compare/v1.1.0...HEAD
[Unreleased]: https://github.com/pybricks/pybricks-code/compare/v1.2.0-beta.1...HEAD
[1.2.0-beta.1]: https://github.com/pybricks/pybricks-code/compare/v1.1.0...v1.2.0-beta.1
[1.1.0]: https://github.com/pybricks/pybricks-code/compare/v1.1.0-rc.1...v1.1.0
[1.1.0-rc.1]: https://github.com/pybricks/pybricks-code/compare/v1.1.0-beta.6...v1.1.0-rc.1
[1.1.0-beta.6]: https://github.com/pybricks/pybricks-code/compare/v1.1.0-beta.5...v1.1.0-beta.6
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pybricks/pybricks-code",
"version": "1.1.0",
"version": "1.2.0-beta.1",
"license": "MIT",
"author": "The Pybricks Authors",
"repository": {
+8 -2
View File
@@ -1,15 +1,16 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import { useDispatch as useReduxDispatch } from 'react-redux';
import { Dispatch as ReduxDispatch } from 'redux';
import { AppAction } from './app/actions';
import { BleDIServiceAction } from './ble-device-info-service/actions';
import { BleUartAction } from './ble-nordic-uart-service/actions';
import {
BlePybricksServiceAction,
BlePybricksServiceCommandAction,
BlePybricksServiceEventAction,
} from './ble-pybricks-service/actions';
import { BleUartAction } from './ble-uart/actions';
import { BLEAction, BLEConnectAction } from './ble/actions';
import { EditorAction } from './editor/actions';
import { FlashFirmwareAction } from './firmware/actions';
@@ -60,3 +61,8 @@ export type Action =
* Dispatch function.
*/
export type Dispatch = ReduxDispatch<Action>;
/**
* Typed version of Redux useDispatch() hook.
*/
export const useDispatch = (): Dispatch => useReduxDispatch<Dispatch>();
+17 -4
View File
@@ -2,11 +2,11 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import { Classes } from '@blueprintjs/core';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import React, { useEffect, useState } from 'react';
import SplitterLayout from 'react-splitter-layout';
import { useDispatch } from '../actions';
import Editor from '../editor/Editor';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import { toggleBoolean } from '../settings/actions';
import { BooleanSettingId } from '../settings/defaults';
import StatusBar from '../status-bar/StatusBar';
@@ -122,9 +122,22 @@ const Docs: React.FunctionComponent = (_props) => {
};
const App: React.FunctionComponent = (_props) => {
const showDocs = useSelector((s: RootState): boolean => s.settings.showDocs);
const darkMode = useSelector((s): boolean => s.settings.darkMode);
const showDocs = useSelector((s): boolean => s.settings.showDocs);
const [isDragging, setIsDragging] = useState(false);
// darkMode class has to be applied to body element, otherwise it won't
// affect portals
useEffect(() => {
if (!darkMode) {
// no class for light mode, so nothing to do
return;
}
document.body.classList.add(Classes.DARK);
return () => document.body.classList.remove(Classes.DARK);
}, [darkMode]);
return (
<div className="pb-app h-100 w-100 p-absolute">
<Toolbar />
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
// actions/ble-uart.ts: Actions for Bluetooth Low Energy nRF UART service
//
// Actions for Bluetooth Low Energy Nordic UART service
import { Action } from 'redux';
+32
View File
@@ -8,6 +8,8 @@ import {
} from '../ble-device-info-service/actions';
import { PnpIdVendorIdSource } from '../ble-device-info-service/protocol';
import { HubType, LegoCompanyId } from '../ble-lwp3-service/protocol';
import { didReceiveStatusReport } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
import {
BleDeviceDidFailToConnectReason,
connect,
@@ -25,7 +27,9 @@ test('initial state', () => {
expect(reducers(undefined, {} as Action)).toMatchInlineSnapshot(`
Object {
"connection": "ble.connection.state.disconnected",
"deviceBatteryCharging": false,
"deviceFirmwareVersion": "",
"deviceLowBatteryWarning": false,
"deviceName": "",
"deviceType": "",
}
@@ -113,3 +117,31 @@ test('deviceFirmwareVersion', () => {
.deviceFirmwareVersion,
).toBe('');
});
test('deviceLowBatteryWarning', () => {
expect(
reducers(
{ deviceLowBatteryWarning: false } as State,
didReceiveStatusReport(statusToFlag(Status.BatteryLowVoltageWarning)),
).deviceLowBatteryWarning,
).toBeTruthy();
expect(
reducers(
{ deviceLowBatteryWarning: true } as State,
didReceiveStatusReport(~statusToFlag(Status.BatteryLowVoltageWarning)),
).deviceLowBatteryWarning,
).toBeFalsy();
expect(
reducers({ deviceLowBatteryWarning: true } as State, didDisconnect())
.deviceLowBatteryWarning,
).toBeFalsy();
});
test('deviceBatteryCharging', () => {
expect(
reducers({ deviceBatteryCharging: true } as State, didDisconnect())
.deviceBatteryCharging,
).toBeFalsy();
});
+27
View File
@@ -8,6 +8,8 @@ import { Reducer, combineReducers } from 'redux';
import { Action } from '../actions';
import { BleDIServiceActionType } from '../ble-device-info-service/actions';
import { getHubTypeName } from '../ble-device-info-service/protocol';
import { BlePybricksServiceEventActionType } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
import { BleDeviceActionType } from './actions';
/**
@@ -85,9 +87,34 @@ const deviceFirmwareVersion: Reducer<string, Action> = (state = '', action) => {
}
};
const deviceLowBatteryWarning: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case BleDeviceActionType.DidDisconnect:
return false;
case BlePybricksServiceEventActionType.DidReceiveStatusReport:
return Boolean(
action.statusFlags & statusToFlag(Status.BatteryLowVoltageWarning),
);
default:
return state;
}
};
const deviceBatteryCharging: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case BleDeviceActionType.DidDisconnect:
return false;
// TODO: hub does not currently have a status flag for this
default:
return state;
}
};
export default combineReducers({
connection,
deviceName,
deviceType,
deviceFirmwareVersion,
deviceLowBatteryWarning,
deviceBatteryCharging,
});
+15 -15
View File
@@ -27,6 +27,18 @@ import {
pnpIdUUID,
softwareRevisionStringUUID,
} from '../ble-device-info-service/protocol';
import {
BleUartActionType,
BleUartWriteAction,
didFailToWrite as didFailToWriteUart,
didNotify as didNotifyUart,
didWrite as didWriteUart,
} from '../ble-nordic-uart-service/actions';
import {
RxCharUUID as uartRxCharUUID,
ServiceUUID as uartServiceUUID,
TxCharUUID as uartTxCharUUID,
} from '../ble-nordic-uart-service/protocol';
import {
BlePybricksServiceActionType,
didFailToWriteCommand,
@@ -37,6 +49,8 @@ import {
ControlCharacteristicUUID as pybricksCommandCharacteristicUUID,
ServiceUUID as pybricksServiceUUID,
} from '../ble-pybricks-service/protocol';
import { RootState } from '../reducers';
import { ensureError } from '../utils';
import {
BLEActionType,
BleDeviceActionType as BLEDeviceActionType,
@@ -49,22 +63,8 @@ import {
didDisconnect,
didFailToConnect,
disconnect as disconnectAction,
} from '../ble/actions';
import { BleConnectionState } from '../ble/reducers';
import { RootState } from '../reducers';
import { ensureError } from '../utils';
import {
BleUartActionType,
BleUartWriteAction,
didFailToWrite as didFailToWriteUart,
didNotify as didNotifyUart,
didWrite as didWriteUart,
} from './actions';
import {
RxCharUUID as uartRxCharUUID,
ServiceUUID as uartServiceUUID,
TxCharUUID as uartTxCharUUID,
} from './protocol';
import { BleConnectionState } from './reducers';
const decoder = new TextDecoder();
+4 -4
View File
@@ -12,10 +12,10 @@ import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eigh
import xcodeTheme from 'monaco-themes/themes/Xcode_default.json';
import React, { useEffect, useRef } from 'react';
import MonacoEditor, { monaco } from 'react-monaco-editor';
import { useDispatch, useSelector } from 'react-redux';
import { IDisposable } from 'xterm';
import { useDispatch } from '../actions';
import { compile } from '../mpy/actions';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import { toggleBoolean } from '../settings/actions';
import { BooleanSettingId } from '../settings/defaults';
import { isMacOS } from '../utils/os';
@@ -63,7 +63,7 @@ const xcodeId = 'xcode';
monaco.editor.defineTheme(xcodeId, xcodeTheme as monaco.editor.IStandaloneThemeData);
const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => {
const editor = useSelector((state: RootState) => state.editor.current);
const editor = useSelector((s) => s.editor.current);
const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en });
@@ -143,7 +143,7 @@ const Editor: React.FunctionComponent = (_props) => {
return () => window.removeEventListener('storage', onStorage);
});
const darkMode = useSelector((state: RootState) => state.settings.darkMode);
const darkMode = useSelector((s) => s.settings.darkMode);
const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en });
+3 -3
View File
@@ -2,9 +2,9 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from '../actions';
import * as notificationActions from '../notifications/actions';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton';
import { TooltipId } from '../toolbar/i18n';
import * as editorActions from './actions';
@@ -13,7 +13,7 @@ import openIcon from './open.svg';
type OpenButtonProps = Pick<OpenFileButtonProps, 'id'>;
const OpenButton: React.FunctionComponent<OpenButtonProps> = (props) => {
const editor = useSelector((state: RootState) => state.editor.current);
const editor = useSelector((s) => s.editor.current);
const dispatch = useDispatch();
return (
+3 -3
View File
@@ -2,9 +2,9 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from '../actions';
import * as editorActions from '../editor/actions';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import downloadIcon from './save.svg';
@@ -13,7 +13,7 @@ type SaveAsButtonProps = Pick<ActionButtonProps, 'id'> &
Pick<ActionButtonProps, 'keyboardShortcut'>;
const SaveAsButton: React.FunctionComponent<SaveAsButtonProps> = (props) => {
const editor = useSelector((state: RootState) => state.editor.current);
const editor = useSelector((s) => s.editor.current);
const dispatch = useDispatch();
+1 -1
View File
@@ -2,8 +2,8 @@
// Copyright (c) 2020 The Pybricks Authors
import { AsyncSaga } from '../../test';
import { didFailToWrite } from '../ble-nordic-uart-service/actions';
import { eventProtocolError } from '../ble-pybricks-service/actions';
import { didFailToWrite } from '../ble-uart/actions';
import {
BleDeviceFailToConnectReasonType,
didFailToConnect as bleDidFailToConnect,
+4 -1
View File
@@ -2,11 +2,14 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import { takeEvery } from 'typed-redux-saga/macro';
import {
BleUartActionType,
BleUartDidFailToWriteAction,
} from '../ble-nordic-uart-service/actions';
import {
BlePybricksServiceEventActionType,
BlePybricksServiceEventProtocolErrorAction,
} from '../ble-pybricks-service/actions';
import { BleUartActionType, BleUartDidFailToWriteAction } from '../ble-uart/actions';
import {
BleDeviceActionType,
BleDeviceDidFailToConnectAction,
+6 -8
View File
@@ -2,11 +2,11 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from '../actions';
import { BleConnectionState } from '../ble/reducers';
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
import * as notificationActions from '../notifications/actions';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton';
import { TooltipId } from '../toolbar/i18n';
import { flashFirmware } from './actions';
@@ -15,12 +15,10 @@ import firmwareIcon from './firmware.svg';
type FlashButtonProps = Pick<OpenFileButtonProps, 'id'>;
const FlashButton: React.FunctionComponent<FlashButtonProps> = (props) => {
const bootloaderConnection = useSelector(
(state: RootState) => state.bootloader.connection,
);
const bleConnection = useSelector((state: RootState) => state.ble.connection);
const flashing = useSelector((state: RootState) => state.firmware.flashing);
const progress = useSelector((state: RootState) => state.firmware.progress);
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const flashing = useSelector((s) => s.firmware.flashing);
const progress = useSelector((s) => s.firmware.progress);
const dispatch = useDispatch();
+4 -6
View File
@@ -2,11 +2,11 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from '../actions';
import { toggleBluetooth } from '../ble/actions';
import { BleConnectionState } from '../ble/reducers';
import { BootloaderConnectionState } from '../lwp3-bootloader/reducers';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import btConnectedIcon from './bt-connected.svg';
@@ -15,10 +15,8 @@ import btDisconnectedIcon from './bt-disconnected.svg';
type BluetoothButtonProps = Pick<ActionButtonProps, 'id'>;
const BluetoothButton: React.FunctionComponent<BluetoothButtonProps> = (props) => {
const bootloaderConnection = useSelector(
(state: RootState) => state.bootloader.connection,
);
const bleConnection = useSelector((state: RootState) => state.ble.connection);
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const isDisconnected =
bootloaderConnection === BootloaderConnectionState.Disconnected &&
+5 -7
View File
@@ -2,8 +2,8 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../reducers';
import { useDispatch } from '../actions';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import { downloadAndRun } from './actions';
@@ -14,11 +14,9 @@ type RunButtonProps = Pick<ActionButtonProps, 'id'> &
Pick<ActionButtonProps, 'keyboardShortcut'>;
const RunButton: React.FunctionComponent<RunButtonProps> = (props) => {
const editor = useSelector((state: RootState) => state.editor.current);
const downloadProgress = useSelector(
(state: RootState) => state.hub.downloadProgress,
);
const runtime = useSelector((state: RootState) => state.hub.runtime);
const editor = useSelector((s) => s.editor.current);
const downloadProgress = useSelector((s) => s.hub.downloadProgress);
const runtime = useSelector((s) => s.hub.runtime);
const dispatch = useDispatch();
+3 -3
View File
@@ -2,8 +2,8 @@
// Copyright (c) 2020-2021 The Pybricks Authors
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../reducers';
import { useDispatch } from '../actions';
import { useSelector } from '../reducers';
import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton';
import { TooltipId } from '../toolbar/i18n';
import { stop } from './actions';
@@ -14,7 +14,7 @@ type StopButtonProps = Pick<ActionButtonProps, 'id'> &
Pick<ActionButtonProps, 'keyboardShortcut'>;
const StopButton: React.FunctionComponent<StopButtonProps> = (props) => {
const runtime = useSelector((state: RootState) => state.hub.runtime);
const runtime = useSelector((s) => s.hub.runtime);
const dispatch = useDispatch();
+5 -1
View File
@@ -4,12 +4,16 @@
import { mock } from 'jest-mock-extended';
import { monaco } from 'react-monaco-editor';
import { AsyncSaga } from '../../test';
import {
BleUartActionType,
BleUartWriteAction,
didWrite,
} from '../ble-nordic-uart-service/actions';
import {
BlePybricksServiceCommandActionType,
BlePybricksServiceCommandSendStopUserProgram,
didSendCommand,
} from '../ble-pybricks-service/actions';
import { BleUartActionType, BleUartWriteAction, didWrite } from '../ble-uart/actions';
import { MpyActionType, didCompile } from '../mpy/actions';
import { createCountFunc } from '../utils/iter';
import {
+7 -7
View File
@@ -13,19 +13,19 @@ import {
takeEvery,
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import {
BleUartActionType,
BleUartDidFailToWriteAction,
BleUartDidWriteAction,
write,
} from '../ble-nordic-uart-service/actions';
import { SafeTxCharLength } from '../ble-nordic-uart-service/protocol';
import {
BlePybricksServiceCommandActionType,
BlePybricksServiceCommandDidFailToSendAction,
BlePybricksServiceCommandDidSendAction,
sendStopUserProgramCommand,
} from '../ble-pybricks-service/actions';
import {
BleUartActionType,
BleUartDidFailToWriteAction,
BleUartDidWriteAction,
write,
} from '../ble-uart/actions';
import { SafeTxCharLength } from '../ble-uart/protocol';
import { BleDeviceActionType } from '../ble/actions';
import {
MpyActionType,
-25
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
import { Classes } from '@blueprintjs/core';
import { I18nContext } from '@shopify/react-i18n';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -41,30 +40,6 @@ const store = createStore(
applyMiddleware(sagaMiddleware, loggerMiddleware),
);
// Hook in blueprints dark mode class to setting
let oldDarkMode = false;
store.subscribe(() => {
const newDarkMode = store.getState().settings.darkMode;
if (newDarkMode !== oldDarkMode) {
if (newDarkMode) {
document.body.classList.add(Classes.DARK);
for (const frame of document.getElementsByTagName('iframe')) {
frame.contentWindow?.document.documentElement.classList.add(
Classes.DARK,
);
}
} else {
document.body.classList.remove(Classes.DARK);
for (const frame of document.getElementsByTagName('iframe')) {
frame.contentWindow?.document.documentElement.classList.remove(
Classes.DARK,
);
}
}
oldDarkMode = newDarkMode;
}
});
// special styling for beta versions
if (appVersion.match(/beta/)) {
document.body.classList.add('pb-beta');
+4 -4
View File
@@ -14,9 +14,9 @@ import {
} from '@blueprintjs/core';
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from '../actions';
import { appName } from '../app/constants';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import { fetchList, select } from './actions';
import { LicenseStringId } from './i18n';
import en from './i18n.en.json';
@@ -31,7 +31,7 @@ type LicenseListPanelProps = {
const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = (
props,
) => {
const licenseList = useSelector((state: RootState) => state.licenses.list);
const licenseList = useSelector((s) => s.licenses.list);
return (
<div className="pb-license-list">
@@ -54,7 +54,7 @@ const LicenseListPanel: React.VoidFunctionComponent<LicenseListPanelProps> = (
};
const LicenseInfoPanel = React.forwardRef<HTMLDivElement>((_props, ref) => {
const licenseInfo = useSelector((state: RootState) => state.licenses.selected);
const licenseInfo = useSelector((s) => s.licenses.selected);
const [i18n] = useI18n({ id: 'license', translations: { en }, fallback: en });
+6
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
import { TypedUseSelectorHook, useSelector as useReduxSelector } from 'react-redux';
import { Reducer, combineReducers } from 'redux';
import app from './app/reducers';
import ble from './ble/reducers';
@@ -31,3 +32,8 @@ export const rootReducer = combineReducers({
type StateFromReducer<R> = R extends Reducer<infer S> ? S : never;
export type RootState = StateFromReducer<typeof rootReducer>;
/**
* Typed version of react-redux useSelector() hook.
*/
export const useSelector: TypedUseSelectorHook<RootState> = useReduxSelector;
+2 -2
View File
@@ -5,7 +5,7 @@ import { all, put } from 'typed-redux-saga/macro';
import { didStart } from './app/actions';
import app from './app/sagas';
import blePybricksService from './ble-pybricks-service/sagas';
import bleUart from './ble-uart/sagas';
import ble from './ble/sagas';
import editor from './editor/sagas';
import errorLog from './error-log/sagas';
import flashFirmware from './firmware/sagas';
@@ -23,7 +23,7 @@ export default function* (): Generator {
yield* all([
app(),
blePybricksService(),
bleUart(),
ble(),
lwp3BootloaderBle(),
lwp3BootloaderProtocol(),
editor(),
+13 -27
View File
@@ -19,8 +19,8 @@ import {
import { Tooltip2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AboutDialog from '../about/AboutDialog';
import { useDispatch } from '../actions';
import { checkForUpdate, installPrompt, reload } from '../app/actions';
import {
pybricksBugReportsUrl,
@@ -30,7 +30,7 @@ import {
tooltipDelay,
} from '../app/constants';
import { pseudolocalize } from '../i18n';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import ExternalLinkIcon from '../utils/ExternalLinkIcon';
import { isMacOS } from '../utils/os';
import { setBoolean, setString, toggleBoolean } from './actions';
@@ -47,31 +47,17 @@ type SettingsProps = {
const SettingsDrawer: React.FunctionComponent<SettingsProps> = (props) => {
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
const showDocs = useSelector((state: RootState) => state.settings.showDocs);
const darkMode = useSelector((state: RootState) => state.settings.darkMode);
const flashCurrentProgram = useSelector(
(state: RootState) => state.settings.flashCurrentProgram,
);
const serviceWorker = useSelector((state: RootState) => state.app.serviceWorker);
const checkingForUpdate = useSelector(
(state: RootState) => state.app.checkingForUpdate,
);
const updateAvailable = useSelector(
(state: RootState) => state.app.updateAvailable,
);
const beforeInstallPrompt = useSelector(
(state: RootState) => state.app.beforeInstallPrompt,
);
const promptingInstall = useSelector(
(state: RootState) => state.app.promptingInstall,
);
const readyForOfflineUse = useSelector(
(state: RootState) => state.app.readyForOfflineUse,
);
const hubName = useSelector((state: RootState) => state.settings.hubName);
const isHubNameValid = useSelector(
(state: RootState) => state.settings.isHubNameValid,
);
const showDocs = useSelector((s) => s.settings.showDocs);
const darkMode = useSelector((s) => s.settings.darkMode);
const flashCurrentProgram = useSelector((s) => s.settings.flashCurrentProgram);
const serviceWorker = useSelector((s) => s.app.serviceWorker);
const checkingForUpdate = useSelector((s) => s.app.checkingForUpdate);
const updateAvailable = useSelector((s) => s.app.updateAvailable);
const beforeInstallPrompt = useSelector((s) => s.app.beforeInstallPrompt);
const promptingInstall = useSelector((s) => s.app.promptingInstall);
const readyForOfflineUse = useSelector((s) => s.app.readyForOfflineUse);
const hubName = useSelector((s) => s.settings.hubName);
const isHubNameValid = useSelector((s) => s.settings.isHubNameValid);
const dispatch = useDispatch();
+50 -14
View File
@@ -1,32 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2021 The Pybricks Authors
import { Button } from '@blueprintjs/core';
import { Classes as Classes2, Popover2 } from '@blueprintjs/popover2';
import { Button, Intent, ProgressBar } from '@blueprintjs/core';
import { Classes as Classes2, Popover2, Popover2Props } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useSelector } from 'react-redux';
import { BleConnectionState } from '../ble/reducers';
import { RootState } from '../reducers';
import { useSelector } from '../reducers';
import { MessageId } from './i18n';
import en from './i18n.en.json';
import './status-bar.scss';
const commonPopoverProps: Partial<Popover2Props> = {
popoverClassName: Classes2.POPOVER2_CONTENT_SIZING,
placement: 'top',
};
const HubInfoButton: React.VFC = (_props) => {
const deviceName = useSelector((state: RootState) => state.ble.deviceName);
const deviceType = useSelector((state: RootState) => state.ble.deviceType);
const deviceFirmwareVersion = useSelector(
(state: RootState) => state.ble.deviceFirmwareVersion,
);
const deviceName = useSelector((s) => s.ble.deviceName);
const deviceType = useSelector((s) => s.ble.deviceType);
const deviceFirmwareVersion = useSelector((s) => s.ble.deviceFirmwareVersion);
const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en });
return (
<Popover2
popoverClassName={Classes2.POPOVER2_CONTENT_SIZING}
rootBoundary="document"
placement="top"
{...commonPopoverProps}
content={
<table className="no-wrap">
<tbody>
@@ -65,8 +65,39 @@ const HubInfoButton: React.VFC = (_props) => {
);
};
const BatteryIndicator: React.VFC = (_props) => {
const charging = useSelector((s) => s.ble.deviceBatteryCharging);
const lowBatteryWarning = useSelector((s) => s.ble.deviceLowBatteryWarning);
const [i18n] = useI18n({ id: 'statusBar', translations: { en }, fallback: en });
return (
<Popover2
{...commonPopoverProps}
content={
<span className="no-wrap">
{i18n.translate(
lowBatteryWarning ? MessageId.BatteryLow : MessageId.BatteryOk,
)}
</span>
}
>
<div className="pb-battery-indicator" style={{ cursor: 'pointer' }}>
<div className="pb-battery-indicator-body">
<ProgressBar
animate={charging}
stripes={charging}
intent={lowBatteryWarning ? Intent.DANGER : Intent.SUCCESS}
/>
</div>
<div className="pb-battery-indicator-tip" />
</div>
</Popover2>
);
};
const StatusBar: React.VFC = (_props) => {
const connection = useSelector((state: RootState) => state.ble.connection);
const connection = useSelector((s) => s.ble.connection);
return (
<div
@@ -75,7 +106,12 @@ const StatusBar: React.VFC = (_props) => {
aria-live="off"
onContextMenu={(e): void => e.preventDefault()}
>
{connection === BleConnectionState.Connected && <HubInfoButton />}
{connection === BleConnectionState.Connected && (
<>
<HubInfoButton />
<BatteryIndicator />
</>
)}
</div>
);
};
+4
View File
@@ -3,5 +3,9 @@
"connectedTo": "Connected to:",
"hubType": "Hub type:",
"firmware": "Firmware:"
},
"battery": {
"low": "Battery is low. Hub will turn off soon.",
"ok": "Battery level is OK."
}
}
+2
View File
@@ -4,6 +4,8 @@
// Status bar translation keys.
export enum MessageId {
BatteryLow = 'battery.low',
BatteryOk = 'battery.ok',
HubInfoConnectedTo = 'hubInfo.connectedTo',
HubInfoHubType = 'hubInfo.hubType',
HubInfoFirmware = 'hubInfo.firmware',
+39
View File
@@ -11,6 +11,7 @@
background-color: $pb-pybricks-blue;
height: $pb-status-bar-height;
width: 100vw;
padding-inline: 5px;
display: flex;
align-items: center;
}
@@ -19,3 +20,41 @@
.pb-status-bar :first-child {
margin-left: auto;
}
.pb-battery-indicator {
display: flex;
flex-direction: row;
align-items: center;
margin: 5px;
}
.pb-battery-indicator-body {
border-radius: 3px;
border: 2px solid $pt-text-color;
background-color: $pt-text-color;
width: 30px;
}
.pb-battery-indicator-body .#{$ns}-progress-bar {
height: 12px;
}
.pb-battery-indicator-body .#{$ns}-progress-bar,
.pb-battery-indicator-body .#{$ns}-progress-meter {
border-radius: 3px;
}
.pb-battery-indicator-tip {
border-top-right-radius: 1px;
border-bottom-right-radius: 1px;
border: 1px solid $pt-text-color;
background-color: $pt-text-color;
width: 3px;
height: 8px;
}
.#{$ns}-dark .pb-battery-indicator-body,
.#{$ns}-dark .pb-battery-indicator-tip {
border-color: $pt-dark-text-color;
background-color: $pt-dark-text-color;
}
+3 -3
View File
@@ -5,10 +5,10 @@ import { Menu, MenuDivider, MenuItem, ResizeSensor } from '@blueprintjs/core';
import { ContextMenu2, ContextMenu2ContentProps } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useContext, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { RootState } from '../reducers';
import { useDispatch } from '../actions';
import { useSelector } from '../reducers';
import { isMacOS } from '../utils/os';
import { TerminalContext } from './TerminalContext';
import { receiveData } from './actions';
@@ -100,7 +100,7 @@ function createContextMenu(
const Terminal: React.FC = (_props) => {
const { xterm, fitAddon } = useMemo(createXTerm, [createXTerm]);
const terminalRef = useRef<HTMLDivElement>(null);
const darkMode = useSelector((state: RootState) => state.settings.darkMode);
const darkMode = useSelector((s) => s.settings.darkMode);
const dispatch = useDispatch();
const terminalStream = useContext(TerminalContext);
+1 -1
View File
@@ -9,7 +9,7 @@ import {
didFailToWrite,
didNotify,
didWrite,
} from '../ble-uart/actions';
} from '../ble-nordic-uart-service/actions';
import { HubChecksumMessageAction, HubMessageActionType } from '../hub/actions';
import { HubRuntimeState } from '../hub/reducers';
import { createCountFunc } from '../utils/iter';
+6 -2
View File
@@ -13,8 +13,12 @@ import {
takeEvery,
} from 'typed-redux-saga/macro';
import { Action } from '../actions';
import { BleUartActionType, BleUartDidNotifyAction, write } from '../ble-uart/actions';
import { SafeTxCharLength } from '../ble-uart/protocol';
import {
BleUartActionType,
BleUartDidNotifyAction,
write,
} from '../ble-nordic-uart-service/actions';
import { SafeTxCharLength } from '../ble-nordic-uart-service/protocol';
import { checksum } from '../hub/actions';
import { HubRuntimeState } from '../hub/reducers';
import { RootState } from '../reducers';