diff --git a/CHANGELOG.md b/CHANGELOG.md index 180aac0b..d4302e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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]. -[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 diff --git a/package.json b/package.json index c85639f0..70a67463 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/actions.ts b/src/actions.ts index bdfb00c3..33b1a6b3 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -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; + +/** + * Typed version of Redux useDispatch() hook. + */ +export const useDispatch = (): Dispatch => useReduxDispatch(); diff --git a/src/app/App.tsx b/src/app/App.tsx index cb652143..bba931ce 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -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 (
diff --git a/src/ble-uart/actions.ts b/src/ble-nordic-uart-service/actions.ts similarity index 95% rename from src/ble-uart/actions.ts rename to src/ble-nordic-uart-service/actions.ts index 71e02f41..6756aa61 100644 --- a/src/ble-uart/actions.ts +++ b/src/ble-nordic-uart-service/actions.ts @@ -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'; diff --git a/src/ble-uart/protocol.ts b/src/ble-nordic-uart-service/protocol.ts similarity index 100% rename from src/ble-uart/protocol.ts rename to src/ble-nordic-uart-service/protocol.ts diff --git a/src/ble/reducers.test.ts b/src/ble/reducers.test.ts index 8427caff..06a0662a 100644 --- a/src/ble/reducers.test.ts +++ b/src/ble/reducers.test.ts @@ -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(); +}); diff --git a/src/ble/reducers.ts b/src/ble/reducers.ts index 449fe567..c14e089b 100644 --- a/src/ble/reducers.ts +++ b/src/ble/reducers.ts @@ -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 = (state = '', action) => { } }; +const deviceLowBatteryWarning: Reducer = (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 = (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, }); diff --git a/src/ble-uart/sagas.ts b/src/ble/sagas.ts similarity index 99% rename from src/ble-uart/sagas.ts rename to src/ble/sagas.ts index e8ff3741..d591a798 100644 --- a/src/ble-uart/sagas.ts +++ b/src/ble/sagas.ts @@ -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(); diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index 435130a9..99f777cf 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -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 }); diff --git a/src/editor/OpenButton.tsx b/src/editor/OpenButton.tsx index 0728701b..b5e77ded 100644 --- a/src/editor/OpenButton.tsx +++ b/src/editor/OpenButton.tsx @@ -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; const OpenButton: React.FunctionComponent = (props) => { - const editor = useSelector((state: RootState) => state.editor.current); + const editor = useSelector((s) => s.editor.current); const dispatch = useDispatch(); return ( diff --git a/src/editor/SaveAsButton.tsx b/src/editor/SaveAsButton.tsx index a676a2f3..26e991d9 100644 --- a/src/editor/SaveAsButton.tsx +++ b/src/editor/SaveAsButton.tsx @@ -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 & Pick; const SaveAsButton: React.FunctionComponent = (props) => { - const editor = useSelector((state: RootState) => state.editor.current); + const editor = useSelector((s) => s.editor.current); const dispatch = useDispatch(); diff --git a/src/error-log/sagas.test.ts b/src/error-log/sagas.test.ts index d980bb03..664e0991 100644 --- a/src/error-log/sagas.test.ts +++ b/src/error-log/sagas.test.ts @@ -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, diff --git a/src/error-log/sagas.ts b/src/error-log/sagas.ts index c4e40ea3..b918f7db 100644 --- a/src/error-log/sagas.ts +++ b/src/error-log/sagas.ts @@ -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, diff --git a/src/firmware/FlashButton.tsx b/src/firmware/FlashButton.tsx index 996a1fa9..781f14b2 100644 --- a/src/firmware/FlashButton.tsx +++ b/src/firmware/FlashButton.tsx @@ -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; const FlashButton: React.FunctionComponent = (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(); diff --git a/src/hub/BluetoothButton.tsx b/src/hub/BluetoothButton.tsx index 3e91b38b..51595388 100644 --- a/src/hub/BluetoothButton.tsx +++ b/src/hub/BluetoothButton.tsx @@ -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; const BluetoothButton: React.FunctionComponent = (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 && diff --git a/src/hub/RunButton.tsx b/src/hub/RunButton.tsx index 409e4719..31961d62 100644 --- a/src/hub/RunButton.tsx +++ b/src/hub/RunButton.tsx @@ -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 & Pick; const RunButton: React.FunctionComponent = (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(); diff --git a/src/hub/StopButton.tsx b/src/hub/StopButton.tsx index c433c0e2..76fb80b1 100644 --- a/src/hub/StopButton.tsx +++ b/src/hub/StopButton.tsx @@ -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 & Pick; const StopButton: React.FunctionComponent = (props) => { - const runtime = useSelector((state: RootState) => state.hub.runtime); + const runtime = useSelector((s) => s.hub.runtime); const dispatch = useDispatch(); diff --git a/src/hub/sagas.test.ts b/src/hub/sagas.test.ts index 0528b4f2..1e2b5ff9 100644 --- a/src/hub/sagas.test.ts +++ b/src/hub/sagas.test.ts @@ -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 { diff --git a/src/hub/sagas.ts b/src/hub/sagas.ts index f1fdae24..ac855a52 100644 --- a/src/hub/sagas.ts +++ b/src/hub/sagas.ts @@ -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, diff --git a/src/index.tsx b/src/index.tsx index 778710fc..18da4b63 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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'); diff --git a/src/licenses/LicenseDialog.tsx b/src/licenses/LicenseDialog.tsx index d99356b8..bb20d3f3 100644 --- a/src/licenses/LicenseDialog.tsx +++ b/src/licenses/LicenseDialog.tsx @@ -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 = ( props, ) => { - const licenseList = useSelector((state: RootState) => state.licenses.list); + const licenseList = useSelector((s) => s.licenses.list); return (
@@ -54,7 +54,7 @@ const LicenseListPanel: React.VoidFunctionComponent = ( }; const LicenseInfoPanel = React.forwardRef((_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 }); diff --git a/src/reducers.ts b/src/reducers.ts index 04c5f38d..d01683df 100644 --- a/src/reducers.ts +++ b/src/reducers.ts @@ -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 extends Reducer ? S : never; export type RootState = StateFromReducer; + +/** + * Typed version of react-redux useSelector() hook. + */ +export const useSelector: TypedUseSelectorHook = useReduxSelector; diff --git a/src/sagas.ts b/src/sagas.ts index 2ccf8d5b..412c14d6 100644 --- a/src/sagas.ts +++ b/src/sagas.ts @@ -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(), diff --git a/src/settings/SettingsDrawer.tsx b/src/settings/SettingsDrawer.tsx index 85221b76..7dd6686f 100644 --- a/src/settings/SettingsDrawer.tsx +++ b/src/settings/SettingsDrawer.tsx @@ -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 = (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(); diff --git a/src/status-bar/StatusBar.tsx b/src/status-bar/StatusBar.tsx index 08a4830a..6f96493d 100644 --- a/src/status-bar/StatusBar.tsx +++ b/src/status-bar/StatusBar.tsx @@ -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 = { + 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 ( @@ -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 ( + + {i18n.translate( + lowBatteryWarning ? MessageId.BatteryLow : MessageId.BatteryOk, + )} + + } + > +
+
+ +
+
+
+ + ); +}; + const StatusBar: React.VFC = (_props) => { - const connection = useSelector((state: RootState) => state.ble.connection); + const connection = useSelector((s) => s.ble.connection); return (
{ aria-live="off" onContextMenu={(e): void => e.preventDefault()} > - {connection === BleConnectionState.Connected && } + {connection === BleConnectionState.Connected && ( + <> + + + + )}
); }; diff --git a/src/status-bar/i18n.en.json b/src/status-bar/i18n.en.json index 1222b2ff..61d9161a 100644 --- a/src/status-bar/i18n.en.json +++ b/src/status-bar/i18n.en.json @@ -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." } } diff --git a/src/status-bar/i18n.ts b/src/status-bar/i18n.ts index 5e128865..48076c76 100644 --- a/src/status-bar/i18n.ts +++ b/src/status-bar/i18n.ts @@ -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', diff --git a/src/status-bar/status-bar.scss b/src/status-bar/status-bar.scss index 52be658d..e823749a 100644 --- a/src/status-bar/status-bar.scss +++ b/src/status-bar/status-bar.scss @@ -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; +} diff --git a/src/terminal/Terminal.tsx b/src/terminal/Terminal.tsx index 0e2aae53..7dcf7b3b 100644 --- a/src/terminal/Terminal.tsx +++ b/src/terminal/Terminal.tsx @@ -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(null); - const darkMode = useSelector((state: RootState) => state.settings.darkMode); + const darkMode = useSelector((s) => s.settings.darkMode); const dispatch = useDispatch(); const terminalStream = useContext(TerminalContext); diff --git a/src/terminal/sagas.test.ts b/src/terminal/sagas.test.ts index 9ef0953c..495eea54 100644 --- a/src/terminal/sagas.test.ts +++ b/src/terminal/sagas.test.ts @@ -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'; diff --git a/src/terminal/sagas.ts b/src/terminal/sagas.ts index c3722ffc..bd5f021f 100644 --- a/src/terminal/sagas.ts +++ b/src/terminal/sagas.ts @@ -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';