toolbar/buttons: consolidate buttons

This commit is contained in:
David Lechner
2022-03-25 15:54:04 -05:00
parent 9d995ceeef
commit 5dd61fa4c3
47 changed files with 493 additions and 278 deletions
+4 -20
View File
@@ -10,11 +10,9 @@ import {
useHotkeys,
} from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useEffect, useMemo, useState } from 'react';
import { tooltipDelay } from '../app/constants';
import { pointerEventsNone } from '../utils/react';
import { I18nId } from './i18n';
const smallScreenThreshold = 700;
@@ -24,9 +22,7 @@ export interface ActionButtonProps {
/** Keyboard shortcut. */
readonly keyboardShortcut?: string;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: I18nId;
/** Tooltip text that appears when hovering over the button and @showProgress is true. */
readonly progressTooltip?: I18nId;
readonly tooltip: string;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
@@ -43,15 +39,12 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
label,
keyboardShortcut,
tooltip,
progressTooltip,
icon,
enabled,
showProgress,
progress,
onAction,
}) => {
const [i18n] = useI18n();
const [isSmallScreen, setIsSmallScreen] = useState(
window.innerWidth <= smallScreenThreshold,
);
@@ -66,15 +59,6 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
const buttonSize = isSmallScreen ? SpinnerSize.SMALL : SpinnerSize.STANDARD;
const tooltipText =
showProgress && progressTooltip
? i18n.translate(progressTooltip, {
percent:
progress === undefined ? '' : i18n.formatPercentage(progress),
})
: i18n.translate(tooltip) +
(keyboardShortcut ? ` (${keyboardShortcut})` : '');
const hotkeys = useMemo(() => {
if (!keyboardShortcut) {
return [];
@@ -86,7 +70,7 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
allowInInput: true,
preventDefault: true,
combo: keyboardShortcut.replaceAll('-', '+'),
label: i18n.translate(tooltip),
label,
onKeyDown: () => {
if (enabled) {
onAction();
@@ -94,13 +78,13 @@ const ActionButton: React.VoidFunctionComponent<ActionButtonProps> = ({
},
},
];
}, [keyboardShortcut, tooltip, enabled, onAction, i18n]);
}, [keyboardShortcut, tooltip, enabled, label, onAction]);
useHotkeys(hotkeys);
return (
<Tooltip2
content={tooltipText}
content={tooltip}
placement="bottom"
hoverOpenDelay={tooltipDelay}
renderTarget={({
+2 -16
View File
@@ -3,12 +3,10 @@
import { Button, IRef, Intent, Spinner, SpinnerSize } from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useEffect, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { tooltipDelay } from '../app/constants';
import { pointerEventsNone } from '../utils/react';
import { I18nId } from './i18n';
const smallScreenThreshold = 700;
export interface OpenFileButtonProps {
@@ -17,7 +15,7 @@ export interface OpenFileButtonProps {
/** The accepted file extension */
readonly fileExtension: string;
/** Tooltip text that appears when hovering over the button. */
readonly tooltip: I18nId;
readonly tooltip: string;
/** Icon shown on the button. */
readonly icon: string;
/** When true or undefined, the button is enabled. */
@@ -49,8 +47,6 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
onReject,
onClick,
}) => {
const [i18n] = useI18n();
const [isSmallScreen, setIsSmallScreen] = useState(
window.innerWidth <= smallScreenThreshold,
);
@@ -101,17 +97,7 @@ const OpenFileButton: React.VoidFunctionComponent<OpenFileButtonProps> = ({
return (
<Tooltip2
content={i18n.translate(
tooltip,
tooltip === I18nId.FlashProgress
? {
percent:
progress === undefined
? ''
: i18n.formatPercentage(progress),
}
: undefined,
)}
content={tooltip}
placement="bottom"
hoverOpenDelay={tooltipDelay}
renderTarget={({
+12 -15
View File
@@ -3,14 +3,14 @@
import { ButtonGroup } from '@blueprintjs/core';
import React, { useState } from 'react';
import FlashButton from '../firmware/FlashButton';
import BluetoothButton from '../hub/BluetoothButton';
import ReplButton from '../hub/ReplButton';
import RunButton from '../hub/RunButton';
import StopButton from '../hub/StopButton';
import SettingsButton from '../settings/SettingsButton';
import SettingsDrawer from '../settings/SettingsDrawer';
import { preventBrowserNativeContextMenu } from '../utils/react';
import BluetoothButton from './buttons/bluetooth/BluetoothButton';
import FlashButton from './buttons/flash/FlashButton';
import ReplButton from './buttons/repl/ReplButton';
import RunButton from './buttons/run/RunButton';
import SettingsButton from './buttons/settings/SettingsButton';
import StopButton from './buttons/stop/StopButton';
import './toolbar.scss';
@@ -24,19 +24,16 @@ const Toolbar: React.VFC = (_props) => {
className="pb-toolbar"
>
<ButtonGroup className="pb-toolbar-group pb-align-left">
<FlashButton label="Flash" />
<BluetoothButton label="Bluetooth" />
<FlashButton />
<BluetoothButton />
</ButtonGroup>
<ButtonGroup className="pb-toolbar-group pb-align-left">
<RunButton label="Run" keyboardShortcut="F5" />
<StopButton label="Stop" keyboardShortcut="F6" />
<ReplButton label="REPL" />
<RunButton />
<StopButton />
<ReplButton />
</ButtonGroup>
<ButtonGroup className="pb-toolbar-group pb-align-right">
<SettingsButton
label="Settings"
onAction={() => setIsSettingsDrawerOpen(true)}
/>
<SettingsButton onAction={() => setIsSettingsDrawerOpen(true)} />
<SettingsDrawer
isOpen={isSettingsDrawerOpen}
onClose={() => setIsSettingsDrawerOpen(false)}
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../../test';
import { toggleBluetooth } from '../../../ble/actions';
import BluetoothButton from './BluetoothButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<BluetoothButton />);
button.getByRole('button', { name: 'Bluetooth' }).click();
expect(dispatch).toHaveBeenCalledWith(toggleBluetooth());
});
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { toggleBluetooth } from '../../../ble/actions';
import { BleConnectionState } from '../../../ble/reducers';
import { BootloaderConnectionState } from '../../../lwp3-bootloader/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import connectedIcon from './connected.svg';
import disconnectedIcon from './disconnected.svg';
import { I18nId } from './i18n';
const BluetoothButton: React.VFC = () => {
const bootloaderConnection = useSelector((s) => s.bootloader.connection);
const bleConnection = useSelector((s) => s.ble.connection);
const isDisconnected =
bootloaderConnection === BootloaderConnectionState.Disconnected &&
bleConnection === BleConnectionState.Disconnected;
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
tooltip={i18n.translate(
isDisconnected ? I18nId.TooltipConnect : I18nId.TooltipDisconnect,
)}
icon={isDisconnected ? disconnectedIcon : connectedIcon}
enabled={isDisconnected || bleConnection === BleConnectionState.Connected}
showProgress={bleConnection === BleConnectionState.Connecting}
onAction={() => dispatch(toggleBluetooth())}
/>
);
};
export default BluetoothButton;
+177
View File
@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50"
height="50"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="bt-connected.svg"
inkscape:export-filename="button.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="light-circle-fill"
osb:paint="solid"
gradientTransform="matrix(1.6201655,0,0,1.6201655,-208.26055,-377.58061)">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5207" />
</linearGradient>
<linearGradient
id="icon-contents"
osb:paint="solid"
gradientTransform="matrix(0.78378378,0,0,0.78378378,-217.21909,-864.32335)">
<stop
style="stop-color:#3880b7;stop-opacity:1;"
offset="0"
id="stop5179" />
</linearGradient>
<linearGradient
id="light-circle-edge"
osb:paint="solid"
gradientTransform="matrix(0.04571075,0,0,0.26284445,-63.602396,-483.67016)">
<stop
style="stop-color:#b7b7b7;stop-opacity:1;"
offset="0"
id="stop5173" />
</linearGradient>
<linearGradient
id="dark-back"
osb:paint="solid"
gradientTransform="translate(1195.9167,-717.54999)">
<stop
style="stop-color:#333333;stop-opacity:1;"
offset="0"
id="stop1439" />
</linearGradient>
<linearGradient
id="dark-front"
osb:paint="solid"
gradientTransform="translate(4643.556,-4.2349475e-6)">
<stop
style="stop-color:#d4d4d4;stop-opacity:1;"
offset="0"
id="stop1389" />
</linearGradient>
<linearGradient
id="light-front"
osb:paint="solid"
gradientTransform="translate(-313.01005,-269.76128)">
<stop
style="stop-color:#4e4e4e;stop-opacity:1;"
offset="0"
id="stop1184" />
</linearGradient>
<linearGradient
id="light-back"
osb:paint="solid"
gradientTransform="translate(-78.252472)">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop1172" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#light-circle-fill"
id="linearGradient5199"
x1="76.890312"
y1="96.730286"
x2="84.212921"
y2="96.730286"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2728572,0,0,1.2728572,-22.909817,-26.355555)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="21.730624"
inkscape:cy="15.927965"
inkscape:document-units="px"
inkscape:current-layer="g1254"
showgrid="false"
inkscape:window-width="1853"
inkscape:window-height="1145"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
units="px"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:snap-global="false"
showguides="true"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<g
transform="matrix(1.5712682,0,0,1.5712682,-114.96952,131.35809)"
id="g1254">
<path
style="fill:none;stroke:url(#linearGradient5199);stroke-width:1.34710717;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 75.46292,99.915389 7.760721,-6.421198 -3.784681,-3.146812 v 12.778601 l 3.805941,-3.380689 -7.856399,-6.463723"
id="path1061"
inkscape:connector-curvature="0"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:1.34710717;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path1063"
sodipodi:type="arc"
sodipodi:cx="82.457184"
sodipodi:cy="96.529022"
sodipodi:rx="2.976866"
sodipodi:ry="2.976866"
sodipodi:start="5.5902437"
sodipodi:end="0.68451919"
d="m 84.747496,94.627391 a 2.976866,2.976866 0 0 1 0.01593,3.783905 l -2.306247,-1.882274 z"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:1.34710717;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 86.772857,92.884335 -1.068213,0.886672 c 1.320158,1.59269 1.328977,3.896842 0.02105,5.499591 -0.0025,0.0015 -0.0062,-3.05e-4 1.077354,0.884102 1.729557,-2.118935 1.716876,-5.165832 -0.03026,-7.270296 z"
id="path1065"
inkscape:connector-curvature="0"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
sodipodi:nodetypes="cccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50"
height="50"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="bt-disconnected.svg"
inkscape:export-filename="button.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="light-circle-fill"
osb:paint="solid"
gradientTransform="matrix(1.6201655,0,0,1.6201655,-208.26055,-377.58061)">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5207" />
</linearGradient>
<linearGradient
id="icon-contents"
osb:paint="solid"
gradientTransform="matrix(0.78378378,0,0,0.78378378,-217.21909,-864.32335)">
<stop
style="stop-color:#3880b7;stop-opacity:1;"
offset="0"
id="stop5179" />
</linearGradient>
<linearGradient
id="light-circle-edge"
osb:paint="solid"
gradientTransform="matrix(0.04571075,0,0,0.26284445,-63.602396,-483.67016)">
<stop
style="stop-color:#b7b7b7;stop-opacity:1;"
offset="0"
id="stop5173" />
</linearGradient>
<linearGradient
id="dark-back"
osb:paint="solid"
gradientTransform="translate(1195.9167,-717.54999)">
<stop
style="stop-color:#333333;stop-opacity:1;"
offset="0"
id="stop1439" />
</linearGradient>
<linearGradient
id="dark-front"
osb:paint="solid"
gradientTransform="translate(4643.556,-4.2349475e-6)">
<stop
style="stop-color:#d4d4d4;stop-opacity:1;"
offset="0"
id="stop1389" />
</linearGradient>
<linearGradient
id="light-front"
osb:paint="solid"
gradientTransform="translate(-313.01005,-269.76128)">
<stop
style="stop-color:#4e4e4e;stop-opacity:1;"
offset="0"
id="stop1184" />
</linearGradient>
<linearGradient
id="light-back"
osb:paint="solid"
gradientTransform="translate(-78.252472)">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop1172" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#light-circle-fill"
id="linearGradient5199"
x1="76.890312"
y1="96.730286"
x2="84.212921"
y2="96.730286"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2728572,0,0,1.2728572,-22.909817,-26.355555)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="30.590127"
inkscape:cy="29.036914"
inkscape:document-units="px"
inkscape:current-layer="g1254"
showgrid="false"
inkscape:window-width="1853"
inkscape:window-height="1145"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
units="px"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:snap-global="false"
showguides="true"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<g
transform="matrix(1.5712682,0,0,1.5712682,-114.96952,131.35809)"
id="g1254">
<path
style="fill:none;stroke:url(#linearGradient5199);stroke-width:1.34710717;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 75.46292,99.915389 7.760721,-6.421198 -3.784681,-3.146812 v 12.778601 l 3.805941,-3.380689 -7.856399,-6.463723"
id="path1061"
inkscape:connector-curvature="0"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { lookup } from '../../test';
import { lookup } from '../../../../test';
import { I18nId } from './i18n';
import en from './translations/en.json';
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
TooltipConnect = 'tooltip.connect',
TooltipDisconnect = 'tooltip.disconnect',
}
@@ -0,0 +1,7 @@
{
"label": "Bluetooth",
"tooltip": {
"connect": "Connect using Bluetooth",
"disconnect": "Disconnect Bluetooth"
}
}
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../../test';
import { flashFirmware } from '../../../firmware/actions';
import FlashButton from './FlashButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<FlashButton />);
button.getByRole('button', { name: 'Flash' }).click();
expect(dispatch).toHaveBeenCalledWith(flashFirmware(null, false, ''));
});
+71
View File
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { BleConnectionState } from '../../../ble/reducers';
import { flashFirmware } from '../../../firmware/actions';
import { BootloaderConnectionState } from '../../../lwp3-bootloader/reducers';
import * as notificationActions from '../../../notifications/actions';
import { useSelector } from '../../../reducers';
import {
useSettingFlashCurrentProgram,
useSettingHubName,
} from '../../../settings/hooks';
import OpenFileButton from '../../../toolbar/OpenFileButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const FlashButton: React.VFC = () => {
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 [isSettingFlashCurrentProgramEnabled] = useSettingFlashCurrentProgram();
const { hubName } = useSettingHubName();
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<OpenFileButton
label={i18n.translate(I18nId.Label)}
fileExtension=".zip"
icon={icon}
tooltip={
progress
? i18n.translate(I18nId.TooltipProgress, {
percent: i18n.formatPercentage(progress),
})
: i18n.translate(I18nId.TooltipAction)
}
enabled={
bootloaderConnection === BootloaderConnectionState.Disconnected &&
bleConnection === BleConnectionState.Disconnected
}
showProgress={flashing}
progress={progress === null ? undefined : progress}
onFile={(data) =>
dispatch(
flashFirmware(data, isSettingFlashCurrentProgramEnabled, hubName),
)
}
onReject={(file) =>
dispatch(
notificationActions.add(
'error',
`'${file.name}' is not a valid firmware file.`,
),
)
}
onClick={() =>
dispatch(
flashFirmware(null, isSettingFlashCurrentProgramEnabled, hubName),
)
}
/>
);
};
export default FlashButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-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();
});
});
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
TooltipAction = 'tooltip.action',
TooltipProgress = 'tooltip.progress',
}
+231
View File
@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50"
height="50"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="firmware.svg"
inkscape:export-filename="button.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="light-circle-fill"
osb:paint="solid"
gradientTransform="matrix(0.04149181,0,0,0.04149181,1.7417234,549.61437)">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5207" />
</linearGradient>
<linearGradient
id="icon-contents"
osb:paint="solid"
gradientTransform="matrix(0.78378378,0,0,0.78378378,-217.21909,-864.32335)">
<stop
style="stop-color:#3880b7;stop-opacity:1;"
offset="0"
id="stop5179" />
</linearGradient>
<linearGradient
id="light-circle-edge"
osb:paint="solid"
gradientTransform="matrix(0.04571075,0,0,0.26284445,-63.602396,-483.67016)">
<stop
style="stop-color:#b7b7b7;stop-opacity:1;"
offset="0"
id="stop5173" />
</linearGradient>
<linearGradient
id="dark-back"
osb:paint="solid"
gradientTransform="translate(1195.9167,-717.54999)">
<stop
style="stop-color:#333333;stop-opacity:1;"
offset="0"
id="stop1439" />
</linearGradient>
<linearGradient
id="dark-front"
osb:paint="solid"
gradientTransform="translate(4643.556,-4.2349475e-6)">
<stop
style="stop-color:#d4d4d4;stop-opacity:1;"
offset="0"
id="stop1389" />
</linearGradient>
<linearGradient
id="light-front"
osb:paint="solid"
gradientTransform="translate(-313.01005,-269.76128)">
<stop
style="stop-color:#4e4e4e;stop-opacity:1;"
offset="0"
id="stop1184" />
</linearGradient>
<linearGradient
id="light-back"
osb:paint="solid"
gradientTransform="translate(-78.252472)">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop1172" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#light-circle-fill"
id="linearGradient5187"
x1="170.49026"
y1="91.974777"
x2="171.5486"
y2="91.974777"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.2770973,0,0,2.2770973,-375.89057,64.428671)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.313708"
inkscape:cx="33.439167"
inkscape:cy="33.623378"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1853"
inkscape:window-height="1145"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
units="px"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:snap-global="false"
showguides="true"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
transform="matrix(0,2.2770972,-2.2770972,0,242.26153,191.39198)"
sodipodi:type="star"
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:0.92954594;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path924"
sodipodi:sides="3"
sodipodi:cx="37.758598"
sodipodi:cy="100.44551"
sodipodi:r1="1.9985147"
sodipodi:r2="0.99925733"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 39.757113,100.44551 -1.498886,0.86538 -1.498886,0.86539 v -1.73077 -1.730764 l 1.498886,0.865383 z"
inkscape:transform-center-y="0.56883747"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:transform-center-x="-5.2936222e-06" />
<path
inkscape:connector-curvature="0"
id="path926"
d="m 13.537299,271.92763 v 3.8731"
style="fill:#4e4e4e;fill-opacity:1;stroke:url(#linearGradient5187);stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<rect
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:0.59935653;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect940"
width="24.01683"
height="10.585944"
x="1.2748805"
y="284.17142"
rx="0"
ry="0"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<rect
ry="0"
rx="0"
y="283.18323"
x="3.4314961"
height="10.585944"
width="2.7715306"
id="rect942"
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:0.20360442;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<rect
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:0.20360442;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect944"
width="2.7715306"
height="10.585944"
x="9.0755405"
y="283.18323"
rx="0"
ry="0"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<rect
ry="0"
rx="0"
y="283.18323"
x="14.719461"
height="10.585944"
width="2.7715306"
id="rect947"
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:0.20360442;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<rect
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:0.20360442;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect949"
width="2.7715306"
height="10.585944"
x="20.363565"
y="283.18323"
rx="0"
ry="0"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -0,0 +1,7 @@
{
"label": "Flash",
"tooltip": {
"action": "Install Pybricks firmware",
"progress": "Flashing… {percent}"
}
}
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../../test';
import { repl } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import ReplButton from './ReplButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<ReplButton />, {
hub: { runtime: HubRuntimeState.Idle },
});
button.getByRole('button', { name: 'REPL' }).click();
expect(dispatch).toHaveBeenCalledWith(repl());
});
+31
View File
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { repl } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const ReplButton: React.VFC = () => {
const enabled = useSelector((s) => s.hub.runtime === HubRuntimeState.Idle);
const [i18n] = useI18n();
const dispatch = useDispatch();
const action = useCallback(() => dispatch(repl()), [dispatch]);
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
tooltip={i18n.translate(I18nId.Tooltip)}
icon={icon}
enabled={enabled}
onAction={action}
/>
);
};
export default ReplButton;
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-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();
});
});
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
Tooltip = 'tooltip',
}
+189
View File
@@ -0,0 +1,189 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50"
height="50"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="repl.svg"
inkscape:export-filename="button.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="light-circle-fill"
osb:paint="solid"
gradientTransform="translate(-111.65281,-194.6407)">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5207" />
</linearGradient>
<linearGradient
id="icon-contents"
osb:paint="solid"
gradientTransform="matrix(0.78378378,0,0,0.78378378,-217.21909,-864.32335)">
<stop
style="stop-color:#3880b7;stop-opacity:1;"
offset="0"
id="stop5179" />
</linearGradient>
<linearGradient
id="light-circle-edge"
osb:paint="solid"
gradientTransform="matrix(0.04571075,0,0,0.26284445,-63.602396,-483.67016)">
<stop
style="stop-color:#b7b7b7;stop-opacity:1;"
offset="0"
id="stop5173" />
</linearGradient>
<linearGradient
id="dark-back"
osb:paint="solid"
gradientTransform="translate(1195.9167,-717.54999)">
<stop
style="stop-color:#333333;stop-opacity:1;"
offset="0"
id="stop1439" />
</linearGradient>
<linearGradient
id="dark-front"
osb:paint="solid"
gradientTransform="translate(4643.556,-4.2349475e-6)">
<stop
style="stop-color:#d4d4d4;stop-opacity:1;"
offset="0"
id="stop1389" />
</linearGradient>
<linearGradient
id="light-front"
osb:paint="solid"
gradientTransform="translate(-313.01005,-269.76128)">
<stop
style="stop-color:#4e4e4e;stop-opacity:1;"
offset="0"
id="stop1184" />
</linearGradient>
<linearGradient
id="light-back"
osb:paint="solid"
gradientTransform="translate(-78.252472)">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop1172" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#light-circle-fill"
id="linearGradient5189"
x1="151.17233"
y1="96.594086"
x2="155.53439"
y2="96.594086"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4800054,0,0,2.5237898,-359.04545,39.652624)" />
<linearGradient
inkscape:collect="always"
xlink:href="#light-circle-fill"
id="linearGradient5191"
x1="147.6288"
y1="96.594086"
x2="151.99086"
y2="96.594086"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4800054,0,0,2.5237898,-358.13592,39.652624)" />
<linearGradient
inkscape:collect="always"
xlink:href="#light-circle-fill"
id="linearGradient5193"
x1="144.08528"
y1="96.594086"
x2="148.44734"
y2="96.594086"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4800054,0,0,2.5237898,-357.22644,39.652624)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="26.727876"
inkscape:cy="22.121513"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1853"
inkscape:window-height="1145"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
units="px"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:snap-global="false"
showguides="true"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:url(#linearGradient5193);stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 0.93974812,276.85042 7.92259998,6.63441 -7.7594,6.53898"
id="path1115"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
sodipodi:nodetypes="ccc"
inkscape:connector-curvature="0"
id="path1135"
d="m 8.8182101,276.85042 7.9225999,6.63441 -7.7593799,6.53898"
style="fill:none;stroke:url(#linearGradient5191);stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
style="fill:none;stroke:url(#linearGradient5189);stroke-width:2.11666656;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 16.696658,276.85042 7.92262,6.63441 -7.7594,6.53898"
id="path1137"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,4 @@
{
"label": "REPL",
"tooltip": "Start REPL in terminal"
}
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../../test';
import { downloadAndRun } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import RunButton from './RunButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<RunButton />, {
app: { hasEditor: true },
hub: { runtime: HubRuntimeState.Idle },
});
button.getByRole('button', { name: 'Run' }).click();
expect(dispatch).toHaveBeenCalledWith(downloadAndRun());
});
+46
View File
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { downloadAndRun } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const RunButton: React.VFC = () => {
const downloadProgress = useSelector((s) => s.hub.downloadProgress);
const runtime = useSelector((s) => s.hub.runtime);
const hasEditor = useSelector((s) => s.app.hasEditor);
const keyboardShortcut = 'F5';
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
keyboardShortcut={keyboardShortcut}
tooltip={
downloadProgress
? i18n.translate(I18nId.TooltipProgress, {
percent: i18n.formatPercentage(downloadProgress),
})
: i18n.translate(I18nId.TooltipAction, { key: keyboardShortcut })
}
icon={icon}
enabled={hasEditor && runtime === HubRuntimeState.Idle}
showProgress={runtime === HubRuntimeState.Loading}
progress={downloadProgress === null ? undefined : downloadProgress}
onAction={() => dispatch(downloadAndRun())}
/>
);
};
export default RunButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-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();
});
});
+8
View File
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
TooltipAction = 'tooltip.action',
TooltipProgress = 'tooltip.progress',
}
+150
View File
@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50"
height="50"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="run.svg"
inkscape:export-filename="button.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="light-circle-fill"
osb:paint="solid"
gradientTransform="matrix(0.52916667,0,0,0.52916667,-54.066878,170.25889)">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5207" />
</linearGradient>
<linearGradient
id="icon-contents"
osb:paint="solid"
gradientTransform="matrix(0.78378378,0,0,0.78378378,-217.21909,-864.32335)">
<stop
style="stop-color:#3880b7;stop-opacity:1;"
offset="0"
id="stop5179" />
</linearGradient>
<linearGradient
id="light-circle-edge"
osb:paint="solid"
gradientTransform="matrix(0.04571075,0,0,0.26284445,-63.602396,-483.67016)">
<stop
style="stop-color:#b7b7b7;stop-opacity:1;"
offset="0"
id="stop5173" />
</linearGradient>
<linearGradient
id="dark-back"
osb:paint="solid"
gradientTransform="translate(1195.9167,-717.54999)">
<stop
style="stop-color:#333333;stop-opacity:1;"
offset="0"
id="stop1439" />
</linearGradient>
<linearGradient
id="dark-front"
osb:paint="solid"
gradientTransform="translate(4643.556,-4.2349475e-6)">
<stop
style="stop-color:#d4d4d4;stop-opacity:1;"
offset="0"
id="stop1389" />
</linearGradient>
<linearGradient
id="light-front"
osb:paint="solid"
gradientTransform="translate(-313.01005,-269.76128)">
<stop
style="stop-color:#4e4e4e;stop-opacity:1;"
offset="0"
id="stop1184" />
</linearGradient>
<linearGradient
id="light-back"
osb:paint="solid"
gradientTransform="translate(-78.252472)">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop1172" />
</linearGradient>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6568542"
inkscape:cx="32.44435"
inkscape:cy="20.159859"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1853"
inkscape:window-height="1145"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
units="px"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:snap-global="false"
showguides="true"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
sodipodi:type="star"
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:2.11666656;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path1017"
sodipodi:sides="3"
sodipodi:cx="10.839368"
sodipodi:cy="283.3425"
sodipodi:r1="11.6466"
sodipodi:r2="5.8232999"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 22.485968,283.3425 -8.73495,5.04312 -8.73495,5.04313 0,-10.08625 0,-10.08625 8.73495,5.04312 z"
inkscape:transform-center-x="-1.4558184"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,7 @@
{
"label": "Run",
"tooltip": {
"action": "Download and run this program ({key})",
"progress": "Downloading… {percent}"
}
}
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../../test';
import SettingsButton from './SettingsButton';
afterEach(() => {
cleanup();
});
it('should invoke callback when clicked', () => {
const handleAction = jest.fn();
const [button] = testRender(<SettingsButton onAction={handleAction} />);
button.getByRole('button', { name: 'Settings' }).click();
expect(handleAction).toHaveBeenCalled();
});
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import ActionButton, { ActionButtonProps } from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
type SettingsButtonProps = Pick<ActionButtonProps, 'onAction'>;
const SettingsButton: React.VoidFunctionComponent<SettingsButtonProps> = ({
onAction,
}) => {
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
tooltip={i18n.translate(I18nId.Tooltip)}
icon={icon}
onAction={onAction}
/>
);
};
export default SettingsButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-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();
});
});
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
Tooltip = 'tooltip',
}
+61
View File
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 50 50"
enable-background="new 0 0 20 20"
xml:space="preserve"
sodipodi:docname="settings.svg"
width="50"
height="50"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
id="metadata12"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs10" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1480"
inkscape:window-height="1046"
id="namedview8"
showgrid="false"
inkscape:pagecheckerboard="true"
inkscape:zoom="11.8"
inkscape:cx="24.88532"
inkscape:cy="16.779661"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" />
<g
id="cog_2_"
transform="matrix(2.25,0,0,2.25,2.5,2.5)"
style="fill:#ffffff;fill-opacity:1">
<g
id="g4"
style="fill:#ffffff;fill-opacity:1">
<path
d="M 19,8 H 16.69 C 16.55,7.54 16.36,7.11 16.13,6.7 L 17.83,5 c 0.39,-0.39 0.39,-1.02 0,-1.41 L 16.42,2.18 c -0.39,-0.39 -1.02,-0.39 -1.41,0 l -1.7,1.7 C 12.9,3.66 12.47,3.47 12.01,3.33 V 1 c 0,-0.55 -0.45,-1 -1,-1 H 9 C 8.45,0 8,0.45 8,1 V 3.33 C 7.52,3.47 7.06,3.67 6.63,3.91 L 5,2.28 C 4.63,1.91 4.02,1.91 3.64,2.28 L 2.28,3.64 C 1.91,4.02 1.91,4.63 2.28,5 L 3.9,6.62 C 3.66,7.06 3.46,7.51 3.31,8 H 1 C 0.45,8 0,8.45 0,9 v 2 c 0,0.55 0.45,1 1,1 h 2.31 c 0.14,0.46 0.33,0.89 0.56,1.3 L 2.17,15 c -0.39,0.39 -0.39,1.02 0,1.41 l 1.41,1.41 c 0.39,0.39 1.02,0.39 1.41,0 l 1.7,-1.7 c 0.41,0.22 0.84,0.41 1.3,0.55 V 19 c 0,0.55 0.45,1 1,1 h 2 c 0.55,0 1,-0.45 1,-1 v -2.33 c 0.48,-0.14 0.94,-0.35 1.37,-0.59 L 15,17.72 c 0.37,0.37 0.98,0.37 1.36,0 l 1.36,-1.36 c 0.37,-0.37 0.37,-0.98 0,-1.36 L 16.1,13.38 C 16.34,12.95 16.55,12.49 16.7,12 H 19 c 0.55,0 1,-0.45 1,-1 V 9 C 20,8.45 19.55,8 19,8 Z m -9,6 C 7.79,14 6,12.21 6,10 6,7.79 7.79,6 10,6 c 2.21,0 4,1.79 4,4 0,2.21 -1.79,4 -4,4 z"
id="path2"
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill-rule:evenodd;fill:#ffffff;fill-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,4 @@
{
"label": "Settings",
"tooltip": "Open settings and help"
}
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { cleanup } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../../test';
import { stop } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import StopButton from './StopButton';
afterEach(() => {
cleanup();
});
it('should dispatch action when clicked', () => {
const [button, dispatch] = testRender(<StopButton />, {
hub: { runtime: HubRuntimeState.Running },
});
button.getByRole('button', { name: 'Stop' }).click();
expect(dispatch).toHaveBeenCalledWith(stop());
});
+33
View File
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { useI18n } from '@shopify/react-i18n';
import React from 'react';
import { useDispatch } from 'react-redux';
import { stop } from '../../../hub/actions';
import { HubRuntimeState } from '../../../hub/reducers';
import { useSelector } from '../../../reducers';
import ActionButton from '../../ActionButton';
import { I18nId } from './i18n';
import icon from './icon.svg';
const StopButton: React.VFC = () => {
const runtime = useSelector((s) => s.hub.runtime);
const keyboardShortcut = 'F6';
const dispatch = useDispatch();
const [i18n] = useI18n();
return (
<ActionButton
label={i18n.translate(I18nId.Label)}
keyboardShortcut={keyboardShortcut}
tooltip={i18n.translate(I18nId.Tooltip, { key: keyboardShortcut })}
icon={icon}
enabled={runtime === HubRuntimeState.Running}
onAction={() => dispatch(stop())}
/>
);
};
export default StopButton;
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-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();
});
});
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
export enum I18nId {
Label = 'label',
Tooltip = 'tooltip',
}
+141
View File
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="50"
height="50"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="stop.svg"
inkscape:export-filename="button.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<linearGradient
id="light-circle-fill"
osb:paint="solid"
gradientTransform="matrix(0.52916667,0,0,0.52916667,-53.400128,172.5108)">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5207" />
</linearGradient>
<linearGradient
id="icon-contents"
osb:paint="solid"
gradientTransform="matrix(0.78378378,0,0,0.78378378,-217.21909,-864.32335)">
<stop
style="stop-color:#3880b7;stop-opacity:1;"
offset="0"
id="stop5179" />
</linearGradient>
<linearGradient
id="light-circle-edge"
osb:paint="solid"
gradientTransform="matrix(0.04571075,0,0,0.26284445,-63.602396,-483.67016)">
<stop
style="stop-color:#b7b7b7;stop-opacity:1;"
offset="0"
id="stop5173" />
</linearGradient>
<linearGradient
id="dark-back"
osb:paint="solid"
gradientTransform="translate(1195.9167,-717.54999)">
<stop
style="stop-color:#333333;stop-opacity:1;"
offset="0"
id="stop1439" />
</linearGradient>
<linearGradient
id="dark-front"
osb:paint="solid"
gradientTransform="translate(4643.556,-4.2349475e-6)">
<stop
style="stop-color:#d4d4d4;stop-opacity:1;"
offset="0"
id="stop1389" />
</linearGradient>
<linearGradient
id="light-front"
osb:paint="solid"
gradientTransform="translate(-313.01005,-269.76128)">
<stop
style="stop-color:#4e4e4e;stop-opacity:1;"
offset="0"
id="stop1184" />
</linearGradient>
<linearGradient
id="light-back"
osb:paint="solid"
gradientTransform="translate(-78.252472)">
<stop
style="stop-color:#eeeeee;stop-opacity:1;"
offset="0"
id="stop1172" />
</linearGradient>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.313708"
inkscape:cx="19.480688"
inkscape:cy="15.933037"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1853"
inkscape:window-height="1145"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
units="px"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:snap-global="false"
showguides="true"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<rect
style="fill:url(#light-circle-fill);fill-opacity:1;stroke:none;stroke-width:1.58749974;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5272"
width="15.200954"
height="15.200954"
x="5.6828184"
y="275.50818"
inkscape:export-filename="out.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,4 @@
{
"label": "Stop",
"tooltip": "Stop everything ({key})"
}
-16
View File
@@ -1,16 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
//
// Toolbar button translation keys.
export enum I18nId {
Run = 'run.action.tooltip',
RunProgress = 'run.progress.tooltip',
Stop = 'stop.tooltip',
Repl = 'repl.tooltip',
Flash = 'flash.action.tooltip',
FlashProgress = 'flash.progress.tooltip',
BluetoothConnect = 'bluetooth.connect.tooltip',
BluetoothDisconnect = 'bluetooth.disconnect.tooltip',
Settings = 'settings.tooltip',
}
-17
View File
@@ -1,17 +0,0 @@
{
"stop": { "tooltip": "Stop everything" },
"run": {
"action": { "tooltip": "Download and run this program" },
"progress": { "tooltip": "Downloading… {percent}" }
},
"repl": { "tooltip": "Start REPL in terminal" },
"bluetooth": {
"connect": { "tooltip": "Connect using Bluetooth" },
"disconnect": { "tooltip": "Disconnect Bluetooth" }
},
"flash": {
"action": { "tooltip": "Install Pybricks firmware" },
"progress": { "tooltip": "Flashing… {percent}" }
},
"settings": { "tooltip": "Settings" }
}