mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 02:24:58 +00:00
firmware: add multi-step dialog
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import './installPybricksDialog.scss';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Classes,
|
||||
ControlGroup,
|
||||
DialogStep,
|
||||
FormGroup,
|
||||
IRef,
|
||||
Icon,
|
||||
InputGroup,
|
||||
Intent,
|
||||
MultistepDialog,
|
||||
NonIdealState,
|
||||
Spinner,
|
||||
Switch,
|
||||
} from '@blueprintjs/core';
|
||||
import { Classes as Classes2, Popover2 } from '@blueprintjs/popover2';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { appName } from '../../app/constants';
|
||||
import HelpButton from '../../components/HelpButton';
|
||||
import {
|
||||
Hub,
|
||||
hubHasBluetoothButton,
|
||||
hubHasExternalFlash,
|
||||
hubHasUSB,
|
||||
} from '../../components/hubPicker';
|
||||
import { HubPicker } from '../../components/hubPicker/HubPicker';
|
||||
import { useSelector } from '../../reducers';
|
||||
import {
|
||||
firmwareInstallPybricksDialogAccept,
|
||||
firmwareInstallPybricksDialogCancel,
|
||||
} from './actions';
|
||||
import { useFirmware } from './hooks';
|
||||
import { I18nId, useI18n } from './i18n';
|
||||
import { validateHubName } from '.';
|
||||
|
||||
const dialogBody = classNames(
|
||||
Classes.DIALOG_BODY,
|
||||
'pb-firmware-installPybricksDialog-body',
|
||||
);
|
||||
|
||||
type SelectHubPanelProps = {
|
||||
hubType: Hub;
|
||||
onChange: (hubType: Hub) => void;
|
||||
};
|
||||
|
||||
const SelectHubPanel: React.VoidFunctionComponent<SelectHubPanelProps> = ({
|
||||
hubType,
|
||||
onChange,
|
||||
}) => {
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<div className={dialogBody}>
|
||||
<p>{i18n.translate(I18nId.SelectHubPanelMessage)}</p>
|
||||
<HubPicker hubType={hubType} onChange={onChange} />
|
||||
<Popover2
|
||||
popoverClassName={Classes2.POPOVER2_CONTENT_SIZING}
|
||||
placement="right-end"
|
||||
content={
|
||||
<div>
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoMindstormsTitle,
|
||||
)}
|
||||
</h3>
|
||||
<ul>
|
||||
<li>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoMindstormsRcx,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoMindstormsNxt,
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoMindstormsEv3,
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpTitle,
|
||||
)}
|
||||
</h3>
|
||||
<ul>
|
||||
<li>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpWedo2,
|
||||
)}
|
||||
<em>*</em>
|
||||
</li>
|
||||
<li>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpDuploTrain,
|
||||
)}
|
||||
<em>*</em>
|
||||
</li>
|
||||
<li>
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpMario,
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<em>
|
||||
*{' '}
|
||||
{i18n.translate(
|
||||
I18nId.SelectHubPanelNotOnListButtonInfoPoweredUpFootnote,
|
||||
)}
|
||||
</em>
|
||||
</div>
|
||||
}
|
||||
renderTarget={({ isOpen: _isOpen, ref, ...targetProps }) => (
|
||||
<Button
|
||||
elementRef={ref as IRef<HTMLButtonElement>}
|
||||
{...targetProps}
|
||||
>
|
||||
{i18n.translate(I18nId.SelectHubPanelNotOnListButtonLabel)}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type AcceptLicensePanelProps = {
|
||||
hubType: Hub;
|
||||
licenseAccepted: boolean;
|
||||
onLicenseAcceptedChanged: (accepted: boolean) => void;
|
||||
};
|
||||
|
||||
const AcceptLicensePanel: React.VoidFunctionComponent<AcceptLicensePanelProps> = ({
|
||||
hubType,
|
||||
licenseAccepted,
|
||||
onLicenseAcceptedChanged,
|
||||
}) => {
|
||||
const { data, error } = useFirmware(hubType);
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<div className={dialogBody}>
|
||||
<div className="pb-firmware-installPybricksDialog-license">
|
||||
<div className="pb-firmware-installPybricksDialog-license-text">
|
||||
{data && <pre>{data.licenseText}</pre>}
|
||||
{!data && (
|
||||
<NonIdealState>
|
||||
{(error &&
|
||||
i18n.translate(
|
||||
I18nId.LicensePanelLicenseTextError,
|
||||
)) || <Spinner />}
|
||||
</NonIdealState>
|
||||
)}
|
||||
</div>
|
||||
<Checkbox
|
||||
label={i18n.translate(I18nId.LicensePanelAcceptCheckboxLabel)}
|
||||
checked={licenseAccepted}
|
||||
onChange={(e) => onLicenseAcceptedChanged(e.currentTarget.checked)}
|
||||
disabled={!data}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SelectOptionsPanelProps = {
|
||||
hubType: Hub;
|
||||
hubName: string;
|
||||
includeProgram: boolean;
|
||||
onChangeHubName(hubName: string): void;
|
||||
onChangeIncludeProgram(includeProgram: boolean): void;
|
||||
};
|
||||
|
||||
const ConfigureOptionsPanel: React.VoidFunctionComponent<SelectOptionsPanelProps> = ({
|
||||
hubType,
|
||||
hubName,
|
||||
includeProgram,
|
||||
onChangeHubName,
|
||||
onChangeIncludeProgram,
|
||||
}) => {
|
||||
const i18n = useI18n();
|
||||
const isHubNameValid = validateHubName(hubName);
|
||||
|
||||
return (
|
||||
<div className={dialogBody}>
|
||||
<FormGroup
|
||||
label={i18n.translate(I18nId.OptionsPanelHubNameLabel)}
|
||||
labelInfo={i18n.translate(I18nId.OptionsPanelHubNameLabelInfo)}
|
||||
>
|
||||
<ControlGroup>
|
||||
<InputGroup
|
||||
id="hub-name-input"
|
||||
value={hubName}
|
||||
onChange={(e) => onChangeHubName(e.currentTarget.value)}
|
||||
onMouseOver={(e) => e.preventDefault()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
intent={isHubNameValid ? Intent.NONE : Intent.DANGER}
|
||||
placeholder="Pybricks Hub"
|
||||
rightElement={
|
||||
isHubNameValid ? undefined : (
|
||||
<Icon
|
||||
icon="error"
|
||||
intent={Intent.DANGER}
|
||||
itemType="div"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<HelpButton
|
||||
helpForLabel={i18n.translate(I18nId.OptionsPanelHubNameLabel)}
|
||||
content={i18n.translate(I18nId.OptionsPanelHubNameHelp)}
|
||||
/>
|
||||
</ControlGroup>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={i18n.translate(I18nId.OptionsPanelCustomMainLabel)}
|
||||
labelInfo={i18n.translate(I18nId.OptionsPanelCustomMainLabelInfo)}
|
||||
>
|
||||
{(hubHasExternalFlash(hubType) && (
|
||||
<p>
|
||||
{i18n.translate(
|
||||
I18nId.OptionsPanelCustomMainNotApplicableMessage,
|
||||
)}
|
||||
</p>
|
||||
)) || (
|
||||
<ControlGroup>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
I18nId.OptionsPanelCustomMainIncludeCurrentProgramLabel,
|
||||
)}
|
||||
checked={includeProgram}
|
||||
onChange={(e) =>
|
||||
onChangeIncludeProgram(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<HelpButton
|
||||
helpForLabel={i18n.translate(
|
||||
I18nId.OptionsPanelCustomMainIncludeCurrentProgramLabel,
|
||||
)}
|
||||
content={i18n.translate(
|
||||
I18nId.OptionsPanelCustomMainIncludeCurrentProgramHelp,
|
||||
{
|
||||
appName,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</ControlGroup>
|
||||
)}
|
||||
</FormGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type BootloaderModePanelProps = {
|
||||
hubType: Hub;
|
||||
};
|
||||
|
||||
const BootloaderModePanel: React.VoidFunctionComponent<BootloaderModePanelProps> = ({
|
||||
hubType,
|
||||
}) => {
|
||||
const i18n = useI18n();
|
||||
|
||||
const { button, light, lightPattern } = useMemo(() => {
|
||||
return {
|
||||
button: i18n.translate(
|
||||
hubHasBluetoothButton(hubType)
|
||||
? I18nId.BootloaderPanelButtonBluetooth
|
||||
: I18nId.BootloaderPanelButtonPower,
|
||||
),
|
||||
light: i18n.translate(
|
||||
hubHasBluetoothButton(hubType)
|
||||
? I18nId.BootloaderPanelLightBluetooth
|
||||
: I18nId.BootloaderPanelLightStatus,
|
||||
),
|
||||
lightPattern: i18n.translate(
|
||||
hubHasBluetoothButton(hubType)
|
||||
? I18nId.BootloaderPanelLightPatternBluetooth
|
||||
: I18nId.BootloaderPanelLightPatternStatus,
|
||||
),
|
||||
};
|
||||
}, [i18n, hubType]);
|
||||
|
||||
return (
|
||||
<div className={dialogBody}>
|
||||
<p>{i18n.translate(I18nId.BootloaderPanelInstruction1)}</p>
|
||||
<ol>
|
||||
{hubHasUSB(hubType) && (
|
||||
<li>{i18n.translate(I18nId.BootloaderPanelStepDisconnectUsb)}</li>
|
||||
)}
|
||||
|
||||
<li>{i18n.translate(I18nId.BootloaderPanelStepPowerOff)}</li>
|
||||
|
||||
{/* City hub has power issues and requires disconnecting motors/sensors */}
|
||||
{hubType === Hub.City && (
|
||||
<li>{i18n.translate(I18nId.BootloaderPanelStepDisconnectIo)}</li>
|
||||
)}
|
||||
|
||||
<li>
|
||||
{i18n.translate(I18nId.BootloaderPanelStepHoldButton, { button })}
|
||||
</li>
|
||||
|
||||
{hubHasUSB(hubType) && (
|
||||
<li>{i18n.translate(I18nId.BootloaderPanelStepConnectUsb)}</li>
|
||||
)}
|
||||
|
||||
<li>
|
||||
{i18n.translate(I18nId.BootloaderPanelStepWaitForLight, {
|
||||
button,
|
||||
light,
|
||||
lightPattern,
|
||||
})}
|
||||
</li>
|
||||
|
||||
<li>
|
||||
{i18n.translate(
|
||||
/* hubs with USB will keep the power on, but other hubs won't */
|
||||
hubHasUSB(hubType)
|
||||
? I18nId.BootloaderPanelStepReleaseButton
|
||||
: I18nId.BootloaderPanelStepKeepHolding,
|
||||
{
|
||||
button,
|
||||
},
|
||||
)}
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
{i18n.translate(I18nId.BootloaderPanelInstruction2, {
|
||||
flashFirmware: (
|
||||
<strong>
|
||||
{i18n.translate(I18nId.FlashFirmwareButtonLabel)}
|
||||
</strong>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultHubType = Hub.Technic;
|
||||
|
||||
export const InstallPybricksDialog: React.VoidFunctionComponent = () => {
|
||||
const { isOpen } = useSelector((s) => s.firmware.installPybricksDialog);
|
||||
const dispatch = useDispatch();
|
||||
const [hubType, setHubType] = useState(defaultHubType);
|
||||
const [hubName, setHubName] = useState('');
|
||||
const [includeProgram, setIncludeProgram] = useState(false);
|
||||
const [licenseAccepted, setLicenseAccepted] = useState(false);
|
||||
const { data } = useFirmware(hubType);
|
||||
const i18n = useI18n();
|
||||
|
||||
return (
|
||||
<MultistepDialog
|
||||
title={i18n.translate(I18nId.Title)}
|
||||
isOpen={isOpen}
|
||||
onClose={() => dispatch(firmwareInstallPybricksDialogCancel())}
|
||||
finalButtonProps={{
|
||||
text: i18n.translate(I18nId.FlashFirmwareButtonLabel),
|
||||
onClick: () =>
|
||||
dispatch(
|
||||
firmwareInstallPybricksDialogAccept(
|
||||
data?.firmwareZip ?? new ArrayBuffer(0),
|
||||
undefined,
|
||||
hubName,
|
||||
),
|
||||
),
|
||||
}}
|
||||
>
|
||||
<DialogStep
|
||||
id="hub"
|
||||
title={i18n.translate(I18nId.SelectHubPanelTitle)}
|
||||
panel={<SelectHubPanel hubType={hubType} onChange={setHubType} />}
|
||||
nextButtonProps={{ text: i18n.translate(I18nId.NextButtonLabel) }}
|
||||
/>
|
||||
<DialogStep
|
||||
id="license"
|
||||
title={i18n.translate(I18nId.LicensePanelTitle)}
|
||||
panel={
|
||||
<AcceptLicensePanel
|
||||
hubType={hubType}
|
||||
licenseAccepted={licenseAccepted}
|
||||
onLicenseAcceptedChanged={setLicenseAccepted}
|
||||
/>
|
||||
}
|
||||
backButtonProps={{ text: i18n.translate(I18nId.BackButtonLabel) }}
|
||||
nextButtonProps={{
|
||||
disabled: !licenseAccepted,
|
||||
text: i18n.translate(I18nId.NextButtonLabel),
|
||||
}}
|
||||
/>
|
||||
<DialogStep
|
||||
id="options"
|
||||
title={i18n.translate(I18nId.OptionsPanelTitle)}
|
||||
panel={
|
||||
<ConfigureOptionsPanel
|
||||
hubType={hubType}
|
||||
hubName={hubName}
|
||||
includeProgram={includeProgram}
|
||||
onChangeHubName={setHubName}
|
||||
onChangeIncludeProgram={setIncludeProgram}
|
||||
/>
|
||||
}
|
||||
backButtonProps={{ text: i18n.translate(I18nId.BackButtonLabel) }}
|
||||
nextButtonProps={{ text: i18n.translate(I18nId.NextButtonLabel) }}
|
||||
/>
|
||||
<DialogStep
|
||||
id="bootloader"
|
||||
title={i18n.translate(I18nId.BootloaderPanelTitle)}
|
||||
panel={<BootloaderModePanel hubType={hubType} />}
|
||||
backButtonProps={{ text: i18n.translate(I18nId.BackButtonLabel) }}
|
||||
/>
|
||||
</MultistepDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { createAction } from '../../actions';
|
||||
|
||||
/** Actions that request the install Pybricks firmware dialog to be shown. */
|
||||
export const firmwareInstallPybricksDialogShow = createAction(() => ({
|
||||
type: 'firmware.installPybricksDialog.action.show',
|
||||
}));
|
||||
|
||||
/** Actions that indicates the user accepted the install Pybricks firmware dialog. */
|
||||
export const firmwareInstallPybricksDialogAccept = createAction(
|
||||
(firmwareZip: ArrayBuffer, customProgram: string | undefined, hubName: string) => ({
|
||||
type: 'firmware.installPybricksDialog.action.accept',
|
||||
firmwareZip,
|
||||
customProgram,
|
||||
hubName,
|
||||
}),
|
||||
);
|
||||
|
||||
/** Actions that indicates the user canceled the install Pybricks firmware dialog. */
|
||||
export const firmwareInstallPybricksDialogCancel = createAction(() => ({
|
||||
type: 'firmware.installPybricksDialog.action.cancel',
|
||||
}));
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
// based on https://usehooks-ts.com/react-hook/use-fetch
|
||||
|
||||
import { FirmwareReader } from '@pybricks/firmware';
|
||||
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
|
||||
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
|
||||
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
|
||||
import { useEffect, useReducer, useRef } from 'react';
|
||||
import { Hub } from '../../components/hubPicker';
|
||||
|
||||
type FirmwareData = {
|
||||
firmwareZip: ArrayBuffer;
|
||||
licenseText: string;
|
||||
};
|
||||
|
||||
interface State {
|
||||
/** The firmware.zip data or undefined if `fetch()` is not complete or on error. */
|
||||
data?: FirmwareData;
|
||||
/** Undefined `fetch()` is not complete yet or was successful, otherwise the error. */
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
type Cache = { [url: string]: FirmwareData };
|
||||
|
||||
// discriminated union type
|
||||
type Action =
|
||||
| { type: 'loading' }
|
||||
| { type: 'fetched'; payload: FirmwareData }
|
||||
| { type: 'error'; payload: Error };
|
||||
|
||||
const firmwareZipMap = new Map<Hub, string>([
|
||||
[Hub.City, cityHubZip],
|
||||
[Hub.Technic, technicHubZip],
|
||||
[Hub.Move, moveHubZip],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Gets Pybricks firmware .zip file for the specified hub type.
|
||||
* @param hubType The hub type.
|
||||
* @returns The current state.
|
||||
*/
|
||||
export function useFirmware(hubType: Hub): State {
|
||||
const url = firmwareZipMap.get(hubType);
|
||||
const cache = useRef<Cache>({});
|
||||
|
||||
// Used to prevent state update if the component is unmounted
|
||||
const cancelRequest = useRef<boolean>(false);
|
||||
|
||||
const initialState: State = {
|
||||
error: undefined,
|
||||
data: undefined,
|
||||
};
|
||||
|
||||
// Keep state logic separated
|
||||
const fetchReducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'loading':
|
||||
return { ...initialState };
|
||||
case 'fetched':
|
||||
return { ...initialState, data: action.payload };
|
||||
case 'error':
|
||||
return { ...initialState, error: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
// Do nothing if the url is not given
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelRequest.current = false;
|
||||
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: 'loading' });
|
||||
|
||||
// If a cache exists for this url, return it
|
||||
if (cache.current[url]) {
|
||||
dispatch({ type: 'fetched', payload: cache.current[url] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const firmwareZip = await response.arrayBuffer();
|
||||
const reader = await FirmwareReader.load(firmwareZip);
|
||||
const licenseText = await reader.readReadMeOss();
|
||||
const data = { firmwareZip, licenseText };
|
||||
|
||||
cache.current[url] = data;
|
||||
if (cancelRequest.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'fetched', payload: data });
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (cancelRequest.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'error', payload: error as Error });
|
||||
}
|
||||
};
|
||||
|
||||
void fetchData();
|
||||
|
||||
// Use the cleanup function for avoiding a possible
|
||||
// state update after the component was unmounted
|
||||
return () => {
|
||||
cancelRequest.current = true;
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../../test';
|
||||
import { I18nId } from './i18n';
|
||||
import en from './translations/en.json';
|
||||
|
||||
describe('Ensure .json file has matches for I18nId', () => {
|
||||
test.each(Object.values(I18nId))('%s', (id) => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021-2022 The Pybricks Authors
|
||||
//
|
||||
// Settings translation keys.
|
||||
|
||||
import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n';
|
||||
|
||||
export function useI18n(): I18n {
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useShopifyI18n();
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export enum I18nId {
|
||||
Title = 'title',
|
||||
SelectHubPanelTitle = 'selectHubPanel.title',
|
||||
SelectHubPanelMessage = 'selectHubPanel.message',
|
||||
SelectHubPanelNotOnListButtonLabel = 'selectHubPanel.notOnListButton.label',
|
||||
SelectHubPanelNotOnListButtonInfoMindstormsTitle = 'selectHubPanel.notOnListButton.info.mindstorms.title',
|
||||
SelectHubPanelNotOnListButtonInfoMindstormsRcx = 'selectHubPanel.notOnListButton.info.mindstorms.rcx',
|
||||
SelectHubPanelNotOnListButtonInfoMindstormsNxt = 'selectHubPanel.notOnListButton.info.mindstorms.nxt',
|
||||
SelectHubPanelNotOnListButtonInfoMindstormsEv3 = 'selectHubPanel.notOnListButton.info.mindstorms.ev3',
|
||||
SelectHubPanelNotOnListButtonInfoPoweredUpTitle = 'selectHubPanel.notOnListButton.info.poweredUp.title',
|
||||
SelectHubPanelNotOnListButtonInfoPoweredUpWedo2 = 'selectHubPanel.notOnListButton.info.poweredUp.wedo2',
|
||||
SelectHubPanelNotOnListButtonInfoPoweredUpDuploTrain = 'selectHubPanel.notOnListButton.info.poweredUp.duploTrain',
|
||||
SelectHubPanelNotOnListButtonInfoPoweredUpMario = 'selectHubPanel.notOnListButton.info.poweredUp.mario',
|
||||
SelectHubPanelNotOnListButtonInfoPoweredUpFootnote = 'selectHubPanel.notOnListButton.info.poweredUp.footnote',
|
||||
LicensePanelTitle = 'licensePanel.title',
|
||||
LicensePanelLicenseTextError = 'licensePanel.licenseText.error',
|
||||
LicensePanelAcceptCheckboxLabel = 'licensePanel.acceptCheckbox.label',
|
||||
OptionsPanelTitle = 'optionsPanel.title',
|
||||
OptionsPanelHubNameLabel = 'optionsPanel.hubName.label',
|
||||
OptionsPanelHubNameLabelInfo = 'optionsPanel.hubName.labelInfo',
|
||||
OptionsPanelHubNameHelp = 'optionsPanel.hubName.help',
|
||||
OptionsPanelHubNameError = 'optionsPanel.hubName.error',
|
||||
OptionsPanelCustomMainLabel = 'optionsPanel.customMain.label',
|
||||
OptionsPanelCustomMainLabelInfo = 'optionsPanel.customMain.labelInfo',
|
||||
OptionsPanelCustomMainNotApplicableMessage = 'optionsPanel.customMain.notApplicable.message',
|
||||
OptionsPanelCustomMainIncludeCurrentProgramLabel = 'optionsPanel.customMain.includeCurrentProgram.label',
|
||||
OptionsPanelCustomMainIncludeCurrentProgramHelp = 'optionsPanel.customMain.includeCurrentProgram.help',
|
||||
BootloaderPanelTitle = 'bootloaderPanel.title',
|
||||
BootloaderPanelInstruction1 = 'bootloaderPanel.instruction1',
|
||||
BootloaderPanelButtonBluetooth = 'bootloaderPanel.button.bluetooth',
|
||||
BootloaderPanelButtonPower = 'bootloaderPanel.button.power',
|
||||
BootloaderPanelLightBluetooth = 'bootloaderPanel.light.bluetooth',
|
||||
BootloaderPanelLightStatus = 'bootloaderPanel.light.status',
|
||||
BootloaderPanelLightPatternBluetooth = 'bootloaderPanel.lightPattern.bluetooth',
|
||||
BootloaderPanelLightPatternStatus = 'bootloaderPanel.lightPattern.status',
|
||||
BootloaderPanelStepDisconnectUsb = 'bootloaderPanel.step.disconnectUsb',
|
||||
BootloaderPanelStepPowerOff = 'bootloaderPanel.step.powerOff',
|
||||
BootloaderPanelStepDisconnectIo = 'bootloaderPanel.step.disconnectIo',
|
||||
BootloaderPanelStepHoldButton = 'bootloaderPanel.step.holdButton',
|
||||
BootloaderPanelStepConnectUsb = 'bootloaderPanel.step.connectUsb',
|
||||
BootloaderPanelStepWaitForLight = 'bootloaderPanel.step.waitForLight',
|
||||
BootloaderPanelStepReleaseButton = 'bootloaderPanel.step.releaseButton',
|
||||
BootloaderPanelStepKeepHolding = 'bootloaderPanel.step.keepHolding',
|
||||
BootloaderPanelInstruction2 = 'bootloaderPanel.instruction2',
|
||||
NextButtonLabel = 'nextButton.label',
|
||||
BackButtonLabel = 'backButton.label',
|
||||
FlashFirmwareButtonLabel = 'flashFirmwareButton.label',
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* Validates the hub name.
|
||||
* @param hubName The hub name.
|
||||
* @returns True if the name if valid, otherwise false.
|
||||
*/
|
||||
export function validateHubName(hubName: string): boolean {
|
||||
const encoded = encoder.encode(hubName);
|
||||
|
||||
// Technically, the max hub name size is determined by each individual
|
||||
// firmware file, so we can't check until the firmware has been selected.
|
||||
// However all firmware currently have 16 bytes allocated (including zero-
|
||||
// termination), so we can hard code the check here to allow notifying the
|
||||
// user earlier for better UX.
|
||||
return encoded.length < 16;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
.pb-firmware-installPybricksDialog {
|
||||
&-body {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
&-license {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: inherit;
|
||||
|
||||
&-text {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { Reducer, combineReducers } from '@reduxjs/toolkit';
|
||||
import {
|
||||
firmwareInstallPybricksDialogAccept,
|
||||
firmwareInstallPybricksDialogCancel,
|
||||
firmwareInstallPybricksDialogShow,
|
||||
} from './actions';
|
||||
|
||||
/** Controls the flash Pybricks firmware dialog open state. */
|
||||
const isOpen: Reducer<boolean> = (state = false, action) => {
|
||||
if (firmwareInstallPybricksDialogShow.matches(action)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firmwareInstallPybricksDialogAccept.matches(action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firmwareInstallPybricksDialogCancel.matches(action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers({ isOpen });
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"title": "Install Pybricks Firmware",
|
||||
"selectHubPanel": {
|
||||
"title": "Select hub type",
|
||||
"message": "Which kind of hub do you want to use?",
|
||||
"notOnListButton": {
|
||||
"label": "My hub is not in the list.",
|
||||
"info": {
|
||||
"mindstorms": {
|
||||
"title": "MINDSTORMS Programmable Bricks",
|
||||
"rcx": "RCX - not enough memory to run Pybricks",
|
||||
"nxt": "NXT - maybe some day",
|
||||
"ev3": "EV3 - supported using VS Code instead of Pybricks Code"
|
||||
},
|
||||
"poweredUp": {
|
||||
"title": "Unsupported Powered Up Hubs",
|
||||
"wedo2": "WeDo 2.0 Smart hub",
|
||||
"duploTrain": "Duplo Train hub",
|
||||
"mario": "Mario/Luigi/Peach",
|
||||
"footnote": "firmware cannot be updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"licensePanel": {
|
||||
"title": "Accept licenses",
|
||||
"licenseText": {
|
||||
"error": "There was a problem while getting the firmware file."
|
||||
},
|
||||
"acceptCheckbox": {
|
||||
"label": "I have read and agree to the license terms and conditions."
|
||||
}
|
||||
},
|
||||
"optionsPanel": {
|
||||
"title": "Configure options",
|
||||
"hubName": {
|
||||
"label": "Hub name",
|
||||
"labelInfo": "(optional)",
|
||||
"help": "Enter a name here to customize the hub name when flashing the firmware. This name will be used in the Bluetooth advertising data and can be used to identify the hub when connecting.",
|
||||
"error": "The name is too long."
|
||||
},
|
||||
"customMain": {
|
||||
"label": "Custom program",
|
||||
"labelInfo": "(optional)",
|
||||
"notApplicable": {
|
||||
"message": "This hub has external flash memory so including a custom program when flashing firmware is not needed."
|
||||
},
|
||||
"includeCurrentProgram": {
|
||||
"label": "Include current program",
|
||||
"help": "Enable to include your program when flashing the firmware or disable to use the default program. Flashing your program along with the firmware will allow you to run your program without being connected to {appName}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bootloaderPanel": {
|
||||
"title": "Place hub in bootloader mode",
|
||||
"instruction1": "To flash the firmware, the hub must be placed in bootloader mode. Follow the steps below to do this:",
|
||||
"button": {
|
||||
"bluetooth": "Bluetooth button",
|
||||
"power": "power button"
|
||||
},
|
||||
"light": {
|
||||
"bluetooth": "Bluetooth light",
|
||||
"status": "hub status light"
|
||||
},
|
||||
"lightPattern": {
|
||||
"bluetooth": "pink-green-blue-off",
|
||||
"status": "light purple"
|
||||
},
|
||||
"step": {
|
||||
"disconnectUsb": "Disconnect the USB cable from the hub.",
|
||||
"powerOff": "Turn off the hub.",
|
||||
"disconnectIo": "Disconnect all motors and sensors from the I/O ports on the hub.",
|
||||
"holdButton": "Press and hold the {button} on the hub.",
|
||||
"connectUsb": "Connect the USB cable.",
|
||||
"waitForLight": "Keep holding the {button} and wait for the {light} to start flashing {lightPattern}. This takes about 5 seconds.",
|
||||
"releaseButton": "Release the {button}",
|
||||
"keepHolding": "Keep holding the {button}."
|
||||
},
|
||||
"instruction2": "Then click the {flashFirmware} button below to connect to the hub and flash the firmware."
|
||||
},
|
||||
"backButton": {
|
||||
"label": "Back"
|
||||
},
|
||||
"nextButton": {
|
||||
"label": "Next"
|
||||
},
|
||||
"flashFirmwareButton": {
|
||||
"label": "Flash Firmware"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user