mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 The Pybricks Authors
|
||||
Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+227
@@ -1,3 +1,5 @@
|
||||
// Configuration file to override create-react-app.
|
||||
//
|
||||
// https://github.com/gsoft-inc/craco/blob/master/packages/craco/README.md#configuration
|
||||
|
||||
const {
|
||||
@@ -6,9 +8,234 @@ const {
|
||||
getLoaders,
|
||||
loaderByName,
|
||||
} = require('@craco/craco');
|
||||
const LicensePlugin = require('license-webpack-plugin').LicenseWebpackPlugin;
|
||||
const satisfies = require('spdx-satisfies');
|
||||
|
||||
// Permissive licenses can be added here. We would like to avoid copyleft.
|
||||
const approvedLicenses = ['0BSD', 'Apache-2.0', 'BSD-3-Clause', 'ISC', 'MIT'];
|
||||
|
||||
/** Converts a package.json "person" to a string.
|
||||
*
|
||||
* Ref: https://docs.npmjs.com/cli/v6/configuring-npm/package-json#people-fields-author-contributors
|
||||
*
|
||||
* @param person a string or "person" object or undefined
|
||||
* @returns A string or undefined.
|
||||
*/
|
||||
function personToString(person) {
|
||||
if (typeof person === 'string' || typeof person === 'undefined') {
|
||||
return person;
|
||||
}
|
||||
|
||||
let str = person.name;
|
||||
if (person.email) {
|
||||
str += ` <${person.email}>`;
|
||||
}
|
||||
if (person.url) {
|
||||
str += ` (${person.url})`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// Some packages don't have a separate license file, so we have to copy the
|
||||
// license here e.g. from the README.
|
||||
|
||||
function fedorIndutnyLicense(year) {
|
||||
return `This software is licensed under the MIT License.
|
||||
|
||||
Copyright Fedor Indutny, ${year}.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.`;
|
||||
}
|
||||
|
||||
const pybricksLicense = `MIT License
|
||||
|
||||
Copyright (c) 2018-2021 The Pybricks Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
`;
|
||||
|
||||
const shopifyLicense = `MIT License
|
||||
|
||||
Copyright (c) 2021 Shopify
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.`;
|
||||
|
||||
const licenseTextOverrides = {
|
||||
'@pybricks/firmware': pybricksLicense,
|
||||
'@pybricks/mpy-cross-v5': pybricksLicense,
|
||||
'@shopify/dates': shopifyLicense,
|
||||
'@shopify/decorators': shopifyLicense,
|
||||
'@shopify/function-enhancers': shopifyLicense,
|
||||
'@shopify/i18n': shopifyLicense,
|
||||
'@shopify/react': shopifyLicense,
|
||||
'@shopify/react-hooks': shopifyLicense,
|
||||
'@shopify/react-i18n': shopifyLicense,
|
||||
'bn.js': fedorIndutnyLicense(2015),
|
||||
brorand: fedorIndutnyLicense(2014),
|
||||
'des.js': fedorIndutnyLicense(2015),
|
||||
elliptic: fedorIndutnyLicense(2014),
|
||||
'hash.js': fedorIndutnyLicense(2014),
|
||||
'hmac-drbg': fedorIndutnyLicense(2017),
|
||||
'miller-rabin': fedorIndutnyLicense(2014),
|
||||
'minimalistic-crypto-utils': fedorIndutnyLicense(2017),
|
||||
gud: `MIT License
|
||||
|
||||
Copyright (c)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.`,
|
||||
isarray: `(MIT)
|
||||
|
||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.`,
|
||||
'popper.js': `The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019 Federico Zivolo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
|
||||
'zen-push': `Copyright (c) 2018 zenparsing (Kevin Smith)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
plugins: [
|
||||
new LicensePlugin({
|
||||
outputFilename: 'static/oss-licenses.json',
|
||||
perChunkOutput: false,
|
||||
renderLicenses: (modules) => {
|
||||
return JSON.stringify(
|
||||
modules
|
||||
.map((m) => ({
|
||||
name: m.packageJson.name,
|
||||
version: m.packageJson.version,
|
||||
author: personToString(m.packageJson.author),
|
||||
license: m.licenseId,
|
||||
licenseText: m.licenseText,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'en')),
|
||||
);
|
||||
},
|
||||
licenseTextOverrides,
|
||||
additionalModules: [
|
||||
{ name: '@pybricks/pybricks-code', directory: __dirname },
|
||||
],
|
||||
unacceptableLicenseTest: (licenseType) =>
|
||||
!satisfies(licenseType, `(${approvedLicenses.join(' OR ')})`),
|
||||
}),
|
||||
],
|
||||
configure: {
|
||||
resolve: {
|
||||
// need 'esnext' first to avoid compile errors
|
||||
|
||||
+9
-2
@@ -1,7 +1,12 @@
|
||||
{
|
||||
"name": "pybricks-code",
|
||||
"name": "@pybricks/pybricks-code",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": "The Pybricks Authors",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pybricks/pybricks-code"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.36.0",
|
||||
"@craco/craco": "^6.0.0",
|
||||
@@ -24,6 +29,7 @@
|
||||
"@types/zen-push": "^0.1.1",
|
||||
"ace-builds": "^1.4.12",
|
||||
"file-saver": "^2.0.5",
|
||||
"license-webpack-plugin": "^2.3.11",
|
||||
"node-sass": "^4.14.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.13.1",
|
||||
@@ -36,6 +42,7 @@
|
||||
"redux": "^4.0.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-saga": "^1.1.3",
|
||||
"spdx-satisfies": "^5.0.0",
|
||||
"typescript": "~4.1.3",
|
||||
"web-vitals": "^1.0.1",
|
||||
"xterm": "^4.9.0",
|
||||
|
||||
+41
-8
@@ -13,8 +13,14 @@ export enum AppActionType {
|
||||
OpenSettings = 'app.action.openSettings',
|
||||
/** Close settings dialog. */
|
||||
CloseSettings = 'app.action.closeSettings',
|
||||
/** Toggle documentation visibility. */
|
||||
ToggleDocs = 'app.action.toggleDocs',
|
||||
/** Open about dialog. */
|
||||
OpenAboutDialog = 'app.action.openAboutDialog',
|
||||
/** Close about dialog. */
|
||||
CloseAboutDialog = 'app.action.closeAboutDialog',
|
||||
/** Open license dialog. */
|
||||
OpenLicenseDialog = 'app.action.openLicenseDialog',
|
||||
/** Close license dialog. */
|
||||
CloseLicenseDialog = 'app.action.closeLicenseDialog',
|
||||
}
|
||||
|
||||
/** Action that indicates the app has just started. */
|
||||
@@ -41,12 +47,36 @@ export function closeSettings(): AppCloseSettingsAction {
|
||||
return { type: AppActionType.CloseSettings };
|
||||
}
|
||||
|
||||
/** Action to toggle documentation visibility. */
|
||||
export type AppToggleDocsAction = Action<AppActionType.ToggleDocs>;
|
||||
/** Action to open the about dialog. */
|
||||
export type AppOpenAboutDialogAction = Action<AppActionType.OpenAboutDialog>;
|
||||
|
||||
/** Creates an action to toggle documentation visibility. */
|
||||
export function toggleDocs(): AppToggleDocsAction {
|
||||
return { type: AppActionType.ToggleDocs };
|
||||
/** Creates an action to open the about dialog. */
|
||||
export function openAboutDialog(): AppOpenAboutDialogAction {
|
||||
return { type: AppActionType.OpenAboutDialog };
|
||||
}
|
||||
|
||||
/** Action to close the about dialog. */
|
||||
export type AppCloseAboutDialogAction = Action<AppActionType.CloseAboutDialog>;
|
||||
|
||||
/** Creates an action to close the about dialog. */
|
||||
export function closeAboutDialog(): AppCloseAboutDialogAction {
|
||||
return { type: AppActionType.CloseAboutDialog };
|
||||
}
|
||||
|
||||
/** Action to open the license dialog. */
|
||||
export type AppOpenLicenseDialogAction = Action<AppActionType.OpenLicenseDialog>;
|
||||
|
||||
/** Creates an action to open the license dialog. */
|
||||
export function openLicenseDialog(): AppOpenLicenseDialogAction {
|
||||
return { type: AppActionType.OpenLicenseDialog };
|
||||
}
|
||||
|
||||
/** Action to close the license dialog. */
|
||||
export type AppCloseLicenseDialogAction = Action<AppActionType.CloseLicenseDialog>;
|
||||
|
||||
/** Creates an action to close the license dialog. */
|
||||
export function closeLicenseDialog(): AppCloseLicenseDialogAction {
|
||||
return { type: AppActionType.CloseLicenseDialog };
|
||||
}
|
||||
|
||||
/** common type for all app actions. */
|
||||
@@ -54,4 +84,7 @@ export type AppAction =
|
||||
| AppStartupAction
|
||||
| AppOpenSettingsAction
|
||||
| AppCloseSettingsAction
|
||||
| AppToggleDocsAction;
|
||||
| AppOpenAboutDialogAction
|
||||
| AppCloseAboutDialogAction
|
||||
| AppOpenLicenseDialogAction
|
||||
| AppCloseLicenseDialogAction;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { BleUartAction } from './ble-uart';
|
||||
import { EditorAction } from './editor';
|
||||
import { FlashFirmwareAction } from './flash-firmware';
|
||||
import { HubAction, HubMessageAction } from './hub';
|
||||
import { LicenseAction } from './license';
|
||||
import {
|
||||
BootloaderConnectionAction,
|
||||
BootloaderDidRequestAction,
|
||||
@@ -36,6 +37,7 @@ export type Action =
|
||||
| FlashFirmwareAction
|
||||
| HubAction
|
||||
| HubMessageAction
|
||||
| LicenseAction
|
||||
| MpyAction
|
||||
| NotificationAction
|
||||
| ServiceWorkerAction
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { LicenseInfo, LicenseList } from '../reducers/license';
|
||||
|
||||
export enum LicenseActionType {
|
||||
DidFetchList = 'license.action.didFetchList',
|
||||
DidFailToFetchList = 'license.action.didFailToFetchList',
|
||||
Select = 'license.action.select',
|
||||
}
|
||||
|
||||
export type LicenseDidFetchListAction = Action<LicenseActionType.DidFetchList> & {
|
||||
list: LicenseList;
|
||||
};
|
||||
|
||||
export function didFetchList(list: LicenseList): LicenseDidFetchListAction {
|
||||
return { type: LicenseActionType.DidFetchList, list };
|
||||
}
|
||||
|
||||
export type LicenseDidFailToFetchListAction = Action<LicenseActionType.DidFailToFetchList> & {
|
||||
reason: Response;
|
||||
};
|
||||
|
||||
export function didFailToFetchList(reason: Response): LicenseDidFailToFetchListAction {
|
||||
return { type: LicenseActionType.DidFailToFetchList, reason };
|
||||
}
|
||||
|
||||
export type LicenseSelectAction = Action<LicenseActionType.Select> & {
|
||||
info: LicenseInfo;
|
||||
};
|
||||
|
||||
export function select(info: LicenseInfo): LicenseSelectAction {
|
||||
return { type: LicenseActionType.Select, info };
|
||||
}
|
||||
|
||||
export type LicenseAction =
|
||||
| LicenseDidFetchListAction
|
||||
| LicenseDidFailToFetchListAction
|
||||
| LicenseSelectAction;
|
||||
@@ -4,8 +4,8 @@
|
||||
import { Action } from 'redux';
|
||||
|
||||
export enum ServiceWorkerActionType {
|
||||
Update = 'serviceWorker.update',
|
||||
Success = 'serviceWorker.success',
|
||||
DidUpdate = 'serviceWorker.didUpdate',
|
||||
DidSucceed = 'serviceWorker.didSucceed',
|
||||
}
|
||||
|
||||
export type ServiceWorkerAction<
|
||||
@@ -14,14 +14,14 @@ export type ServiceWorkerAction<
|
||||
registration: ServiceWorkerRegistration;
|
||||
};
|
||||
|
||||
export function update(
|
||||
export function didUpdate(
|
||||
registration: ServiceWorkerRegistration,
|
||||
): ServiceWorkerAction<ServiceWorkerActionType.Update> {
|
||||
return { type: ServiceWorkerActionType.Update, registration };
|
||||
): ServiceWorkerAction<ServiceWorkerActionType.DidUpdate> {
|
||||
return { type: ServiceWorkerActionType.DidUpdate, registration };
|
||||
}
|
||||
|
||||
export function success(
|
||||
export function didSucceed(
|
||||
registration: ServiceWorkerRegistration,
|
||||
): ServiceWorkerAction<ServiceWorkerActionType.Success> {
|
||||
return { type: ServiceWorkerActionType.Success, registration };
|
||||
): ServiceWorkerAction<ServiceWorkerActionType.DidSucceed> {
|
||||
return { type: ServiceWorkerActionType.DidSucceed, registration };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { Action } from 'redux';
|
||||
import { SettingId } from '../settings';
|
||||
import { SettingId } from '../settings/user';
|
||||
|
||||
/** Actions related to settings. */
|
||||
export enum SettingsActionType {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// The about dialog
|
||||
|
||||
import { AnchorButton, Button, Classes, Dialog } from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { closeAboutDialog, openLicenseDialog } from '../actions/app';
|
||||
import { RootState } from '../reducers';
|
||||
import {
|
||||
appName,
|
||||
legoDisclaimer,
|
||||
pybricksCopyright,
|
||||
pybricksWebsiteUrl,
|
||||
} from '../settings/ui';
|
||||
import ExternalLinkIcon from './ExternalLinkIcon';
|
||||
import LicenseDialog from './LicenseDialog';
|
||||
import { AboutStringId } from './about-i18n';
|
||||
import en from './about-i18n.en.json';
|
||||
|
||||
import './about.scss';
|
||||
|
||||
type StateProps = { showAboutDialog: boolean };
|
||||
|
||||
type DispatchProps = { onClose: () => void; onLicenseButtonClick: () => void };
|
||||
|
||||
type AboutDialogProps = StateProps & DispatchProps & WithI18nProps;
|
||||
|
||||
class AboutDialog extends React.Component<AboutDialogProps> {
|
||||
render(): JSX.Element {
|
||||
const { i18n, showAboutDialog, onClose, onLicenseButtonClick } = this.props;
|
||||
return (
|
||||
<Dialog title={appName} isOpen={showAboutDialog} onClose={() => onClose()}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<div className="pb-about-icon">
|
||||
<img src="favicon.ico" />
|
||||
</div>
|
||||
<p>
|
||||
<strong>{i18n.translate(AboutStringId.Description)}</strong>
|
||||
</p>
|
||||
<p>{pybricksCopyright}</p>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<p>
|
||||
<small>{legoDisclaimer}</small>
|
||||
</p>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={() => onLicenseButtonClick()}>
|
||||
{i18n.translate(AboutStringId.LicenseButtonLabel)}
|
||||
</Button>
|
||||
<AnchorButton href={pybricksWebsiteUrl} target="blank_">
|
||||
{i18n.translate(AboutStringId.WebsiteButtonLabel)}
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
</div>
|
||||
</div>
|
||||
<LicenseDialog />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
showAboutDialog: state.app.showAboutDialog,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onClose: (): Action => dispatch(closeAboutDialog()),
|
||||
onLicenseButtonClick: (): Action => dispatch(openLicenseDialog()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(withI18n({ id: 'about', fallback: en, translations: { en } })(AboutDialog));
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { tooltipDelay } from '../settings/ui';
|
||||
import { TooltipId } from './button-i18n';
|
||||
import en from './button-i18n.en.json';
|
||||
|
||||
@@ -42,7 +43,11 @@ class ActionButton extends React.Component<Props> {
|
||||
tooltipText += ` (${this.props.keyboardShortcut})`;
|
||||
}
|
||||
return (
|
||||
<Tooltip content={tooltipText} position={Position.BOTTOM}>
|
||||
<Tooltip
|
||||
content={tooltipText}
|
||||
position={Position.BOTTOM}
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Button
|
||||
ref={this.buttonRef}
|
||||
intent={Intent.PRIMARY}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// Icon for indicating external links
|
||||
|
||||
import { Icon } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
|
||||
import './external-link.scss';
|
||||
|
||||
class ExternalLinkIcon extends React.Component {
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<sup className="pb-external-link">
|
||||
<Icon icon="share" iconSize={12} />
|
||||
</sup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExternalLinkIcon;
|
||||
@@ -0,0 +1,163 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// The license dialog
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Callout,
|
||||
Card,
|
||||
Classes,
|
||||
Dialog,
|
||||
NonIdealState,
|
||||
} from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { closeLicenseDialog } from '../actions/app';
|
||||
import { select } from '../actions/license';
|
||||
import { RootState } from '../reducers';
|
||||
import { LicenseInfo, LicenseList } from '../reducers/license';
|
||||
import { appName } from '../settings/ui';
|
||||
import { LicenseStringId } from './license-i18n';
|
||||
import en from './license-i18n.en.json';
|
||||
|
||||
import './license.scss';
|
||||
|
||||
type StateProps = {
|
||||
showLicenseDialog: boolean;
|
||||
licenseList: LicenseList | null;
|
||||
licenseInfo: LicenseInfo | null;
|
||||
};
|
||||
|
||||
type DispatchProps = {
|
||||
onClose: () => void;
|
||||
onSelectPackage: (info: LicenseInfo) => void;
|
||||
};
|
||||
|
||||
type LicenseDialogProps = StateProps & DispatchProps & WithI18nProps;
|
||||
|
||||
class LicenseDialog extends React.Component<LicenseDialogProps> {
|
||||
render(): JSX.Element {
|
||||
const infoDiv = React.createRef<HTMLDivElement>();
|
||||
|
||||
const {
|
||||
i18n,
|
||||
showLicenseDialog,
|
||||
onClose,
|
||||
licenseList,
|
||||
licenseInfo,
|
||||
onSelectPackage,
|
||||
} = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
title={i18n.translate(LicenseStringId.Title)}
|
||||
isOpen={showLicenseDialog}
|
||||
onClose={() => onClose()}
|
||||
className="pb-license-dialog"
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<Callout className={Classes.INTENT_PRIMARY} icon="info-sign">
|
||||
{i18n.translate(LicenseStringId.Description, {
|
||||
name: appName,
|
||||
})}
|
||||
</Callout>
|
||||
<Callout className="pb-license-browser">
|
||||
<div className="pb-license-list">
|
||||
{licenseList === null ? (
|
||||
// TODO: this should be translated and hooked to
|
||||
// state indicating if download is in progress
|
||||
// or there was an actual failure.
|
||||
<NonIdealState>
|
||||
Failed to load license data.
|
||||
</NonIdealState>
|
||||
) : (
|
||||
<ButtonGroup
|
||||
minimal={true}
|
||||
vertical={true}
|
||||
alignText="left"
|
||||
>
|
||||
{licenseList.map((info) => (
|
||||
<Button
|
||||
key={`${info.name}@${info.version}`}
|
||||
onClick={() => {
|
||||
infoDiv.current?.scrollTo(0, 0);
|
||||
onSelectPackage(info);
|
||||
}}
|
||||
>
|
||||
{info.name}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</div>
|
||||
<div className="pb-license-info" ref={infoDiv}>
|
||||
{licenseInfo == null ? (
|
||||
<NonIdealState>
|
||||
{i18n.translate(LicenseStringId.SelectPackageHelp)}
|
||||
</NonIdealState>
|
||||
) : (
|
||||
<div>
|
||||
<Card>
|
||||
<p>
|
||||
<strong>
|
||||
{i18n.translate(
|
||||
LicenseStringId.PackageLabel,
|
||||
)}
|
||||
</strong>{' '}
|
||||
{licenseInfo.name}{' '}
|
||||
<span className={Classes.TEXT_MUTED}>
|
||||
v{licenseInfo.version}
|
||||
</span>
|
||||
</p>
|
||||
{licenseInfo.author && (
|
||||
<p>
|
||||
<strong>
|
||||
{' '}
|
||||
{i18n.translate(
|
||||
LicenseStringId.AuthorLabel,
|
||||
)}
|
||||
</strong>{' '}
|
||||
{licenseInfo.author}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<strong>
|
||||
{' '}
|
||||
{i18n.translate(
|
||||
LicenseStringId.LicenseLabel,
|
||||
)}
|
||||
</strong>{' '}
|
||||
{licenseInfo.license}
|
||||
</p>
|
||||
</Card>
|
||||
<div className="pb-license-text">
|
||||
<pre>{licenseInfo.licenseText}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
showLicenseDialog: state.app.showLicenseDialog,
|
||||
licenseList: state.license.list,
|
||||
licenseInfo: state.license.selected,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
onClose: (): Action => dispatch(closeLicenseDialog()),
|
||||
onSelectPackage: (info): Action => dispatch(select(info)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(withI18n({ id: 'license', fallback: en, translations: { en } })(LicenseDialog));
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { tooltipDelay } from '../settings/ui';
|
||||
import { TooltipId } from './button-i18n';
|
||||
import en from './button-i18n.en.json';
|
||||
|
||||
@@ -28,6 +29,7 @@ class LinkButton extends React.Component<Props> {
|
||||
<Tooltip
|
||||
content={this.props.i18n.translate(this.props.tooltip)}
|
||||
position={Position.BOTTOM}
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import Dropzone, { FileRejection } from 'react-dropzone';
|
||||
import { tooltipDelay } from '../settings/ui';
|
||||
import { TooltipId } from './button-i18n';
|
||||
import en from './button-i18n.en.json';
|
||||
|
||||
@@ -81,6 +82,7 @@ class OpenFileButton extends React.Component<Props> {
|
||||
<Tooltip
|
||||
content={this.props.i18n.translate(this.props.tooltip)}
|
||||
position={Position.BOTTOM}
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Button
|
||||
{...getRootProps()}
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { Drawer, FormGroup, Position, Switch, Tooltip } from '@blueprintjs/core';
|
||||
import {
|
||||
AnchorButton,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Classes,
|
||||
Drawer,
|
||||
FormGroup,
|
||||
Position,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from '@blueprintjs/core';
|
||||
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Action, Dispatch } from '../actions';
|
||||
import { closeSettings } from '../actions/app';
|
||||
import { closeSettings, openAboutDialog } from '../actions/app';
|
||||
import { setBoolean } from '../actions/settings';
|
||||
import { RootState } from '../reducers';
|
||||
import { SettingId } from '../settings';
|
||||
import {
|
||||
pybricksBugReportsUrl,
|
||||
pybricksGitterUrl,
|
||||
pybricksSupportUrl,
|
||||
tooltipDelay,
|
||||
} from '../settings/ui';
|
||||
import { SettingId } from '../settings/user';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import AboutDialog from './AboutDialog';
|
||||
import ExternalLinkIcon from './ExternalLinkIcon';
|
||||
import { SettingsStringId } from './settings-i18n';
|
||||
import en from './settings-i18n.en.json';
|
||||
|
||||
import './settings.scss';
|
||||
|
||||
const tooltipDelay = 1000;
|
||||
|
||||
type StateProps = {
|
||||
open: boolean;
|
||||
showDocs: boolean;
|
||||
@@ -30,6 +44,7 @@ type DispatchProps = {
|
||||
onShowDocsChanged: (checked: boolean) => void;
|
||||
onDarkModeChanged: (checked: boolean) => void;
|
||||
onFlashCurrentProgramChanged: (checked: boolean) => void;
|
||||
onAbout: () => void;
|
||||
};
|
||||
|
||||
type SettingsProps = StateProps & DispatchProps & WithI18nProps;
|
||||
@@ -46,6 +61,7 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
|
||||
onDarkModeChanged,
|
||||
flashCurrentProgram,
|
||||
onFlashCurrentProgramChanged,
|
||||
onAbout,
|
||||
} = this.props;
|
||||
return (
|
||||
<Drawer
|
||||
@@ -55,83 +71,132 @@ class SettingsDrawer extends React.PureComponent<SettingsProps> {
|
||||
title={i18n.translate(SettingsStringId.Title)}
|
||||
onClose={() => onClose()}
|
||||
>
|
||||
<div className="pb-settings">
|
||||
<FormGroup
|
||||
label={i18n.translate(SettingsStringId.AppearanceTitle)}
|
||||
helperText={i18n.translate(
|
||||
SettingsStringId.AppearanceZoomHelp,
|
||||
{
|
||||
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
|
||||
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
content={i18n.translate(
|
||||
SettingsStringId.AppearanceDocumentationTooltip,
|
||||
<div className={Classes.DRAWER_BODY}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<FormGroup
|
||||
label={i18n.translate(SettingsStringId.AppearanceTitle)}
|
||||
helperText={i18n.translate(
|
||||
SettingsStringId.AppearanceZoomHelp,
|
||||
{
|
||||
in: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}-+</span>,
|
||||
out: <span>{isMacOS() ? 'Cmd' : 'Ctrl'}--</span>,
|
||||
},
|
||||
)}
|
||||
position={Position.LEFT}
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
SettingsStringId.AppearanceDocumentationLabel,
|
||||
<Tooltip
|
||||
content={i18n.translate(
|
||||
SettingsStringId.AppearanceDocumentationTooltip,
|
||||
)}
|
||||
large={true}
|
||||
checked={showDocs}
|
||||
onChange={(e) =>
|
||||
onShowDocsChanged(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={i18n.translate(
|
||||
SettingsStringId.AppearanceDarkModeTooltip,
|
||||
)}
|
||||
position={Position.LEFT}
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
boundary="window"
|
||||
position={Position.LEFT}
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
SettingsStringId.AppearanceDocumentationLabel,
|
||||
)}
|
||||
checked={showDocs}
|
||||
onChange={(e) =>
|
||||
onShowDocsChanged(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={i18n.translate(
|
||||
SettingsStringId.AppearanceDarkModeTooltip,
|
||||
)}
|
||||
boundary="window"
|
||||
position={Position.LEFT}
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
SettingsStringId.AppearanceDarkModeLabel,
|
||||
)}
|
||||
checked={darkMode}
|
||||
onChange={(e) =>
|
||||
onDarkModeChanged(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={i18n.translate(SettingsStringId.FirmwareTitle)}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
SettingsStringId.AppearanceDarkModeLabel,
|
||||
<Tooltip
|
||||
content={i18n.translate(
|
||||
SettingsStringId.FirmwareCurrentProgramTooltip,
|
||||
)}
|
||||
large={true}
|
||||
checked={darkMode}
|
||||
onChange={(e) =>
|
||||
onDarkModeChanged(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormGroup>
|
||||
<FormGroup label={i18n.translate(SettingsStringId.FirmwareTitle)}>
|
||||
<Tooltip
|
||||
content={i18n.translate(
|
||||
SettingsStringId.FirmwareCurrentProgramTooltip,
|
||||
)}
|
||||
position={Position.LEFT}
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
SettingsStringId.FirmwareCurrentProgramLabel,
|
||||
)}
|
||||
large={true}
|
||||
checked={flashCurrentProgram}
|
||||
onChange={(e) =>
|
||||
onFlashCurrentProgramChanged(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormGroup>
|
||||
boundary="window"
|
||||
position={Position.LEFT}
|
||||
targetTagName="div"
|
||||
hoverOpenDelay={tooltipDelay}
|
||||
>
|
||||
<Switch
|
||||
label={i18n.translate(
|
||||
SettingsStringId.FirmwareCurrentProgramLabel,
|
||||
)}
|
||||
checked={flashCurrentProgram}
|
||||
onChange={(e) =>
|
||||
onFlashCurrentProgramChanged(
|
||||
(e.target as HTMLInputElement).checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</FormGroup>
|
||||
<FormGroup label={i18n.translate(SettingsStringId.HelpTitle)}>
|
||||
<ButtonGroup
|
||||
minimal={true}
|
||||
vertical={true}
|
||||
alignText="left"
|
||||
>
|
||||
<AnchorButton
|
||||
icon="help"
|
||||
href={pybricksSupportUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(SettingsStringId.HelpSupportLabel)}
|
||||
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton
|
||||
icon="chat"
|
||||
href={pybricksGitterUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(SettingsStringId.HelpChatLabel)}
|
||||
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<AnchorButton
|
||||
icon="virus"
|
||||
href={pybricksBugReportsUrl}
|
||||
target="blank_"
|
||||
>
|
||||
{i18n.translate(SettingsStringId.HelpBugsLabel)}
|
||||
|
||||
<ExternalLinkIcon />
|
||||
</AnchorButton>
|
||||
<Button
|
||||
icon="info-sign"
|
||||
onClick={() => {
|
||||
onAbout();
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
{i18n.translate(SettingsStringId.HelpAboutLabel)}
|
||||
</Button>
|
||||
<AboutDialog />
|
||||
</ButtonGroup>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
@@ -153,6 +218,7 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
||||
dispatch(setBoolean(SettingId.DarkMode, checked)),
|
||||
onFlashCurrentProgramChanged: (checked): Action =>
|
||||
dispatch(setBoolean(SettingId.FlashCurrentProgram, checked)),
|
||||
onAbout: (): Action => dispatch(openAboutDialog()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"about": {
|
||||
"description": "A simple web app for programming LEGO® Powered Up smart hubs using Pybricks MicroPython.",
|
||||
"licenseButton": { "label": "Open Source Licenses" },
|
||||
"websiteButton": { "label": "Pybricks Website" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../test';
|
||||
import { AboutStringId } from './about-i18n';
|
||||
import en from './about-i18n.en.json';
|
||||
|
||||
describe('Ensure .json file has matches for AboutStringId', () => {
|
||||
test.each(Object.values(AboutStringId))('%s', (id) => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// About dialog translation keys.
|
||||
|
||||
export enum AboutStringId {
|
||||
Description = 'about.description',
|
||||
LicenseButtonLabel = 'about.licenseButton.label',
|
||||
WebsiteButtonLabel = 'about.websiteButton.label',
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
.pb-about-icon {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
@import '../variables.scss';
|
||||
|
||||
.bp3-dark .splitter-layout > .layout-splitter {
|
||||
.#{$ns}-dark .splitter-layout > .layout-splitter {
|
||||
// make layout splitter match app color scheme
|
||||
background-color: $pt-dark-app-background-color;
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
background-color: $pt-app-background-color;
|
||||
}
|
||||
|
||||
.bp3-dark .terminal-padding {
|
||||
.#{$ns}-dark .terminal-padding {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
@import '../variables.scss';
|
||||
|
||||
.bp3-dark .ace_gutter {
|
||||
.#{$ns}-dark .ace_gutter {
|
||||
// make ace editor match app backgound color
|
||||
background-color: $pt-dark-app-background-color;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
@import '../variables.scss';
|
||||
|
||||
// override some button magic in case this is used in a button
|
||||
.#{$ns}-button .pb-external-link .#{$ns}-icon:first-child:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"license": {
|
||||
"title": "Open Source Licenses",
|
||||
"description": "{name} is built on open source software. By using {name} you are agreeing to the terms and conditions of all of the included software licenses.",
|
||||
"packageLabel": "Package:",
|
||||
"authorLabel": "Author:",
|
||||
"licenseLabel": "License:",
|
||||
"help": {
|
||||
"selectPackage": "Select a package to view the license."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { lookup } from '../../test';
|
||||
import { LicenseStringId } from './license-i18n';
|
||||
import en from './license-i18n.en.json';
|
||||
|
||||
describe('Ensure .json file has matches for LicenseStringId', () => {
|
||||
test.each(Object.values(LicenseStringId))('%s', (id) => {
|
||||
expect(lookup(en, id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// License dialog translation keys.
|
||||
|
||||
export enum LicenseStringId {
|
||||
Title = 'license.title',
|
||||
Description = 'license.description',
|
||||
PackageLabel = 'license.packageLabel',
|
||||
AuthorLabel = 'license.authorLabel',
|
||||
LicenseLabel = 'license.licenseLabel',
|
||||
SelectPackageHelp = 'license.help.selectPackage',
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
// Custom styling for the LicenseDialog control.
|
||||
|
||||
@import '../variables.scss';
|
||||
|
||||
.pb-license-dialog {
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
.pb-license-browser {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.pb-license-list {
|
||||
width: 25%;
|
||||
flex-flow: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pb-license-info {
|
||||
width: 75%;
|
||||
flex-flow: column;
|
||||
padding: 0px 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pb-license-text {
|
||||
padding: 0px 16px;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"title": "Settings & Help",
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"documentation": {
|
||||
@@ -21,6 +21,21 @@
|
||||
"label": "Include current program",
|
||||
"tooltip": "Select to include your program when installing the firmware."
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"title": "Help",
|
||||
"support": {
|
||||
"label": "Support"
|
||||
},
|
||||
"chat": {
|
||||
"label": "Chat"
|
||||
},
|
||||
"bugs": {
|
||||
"label": "Bug Reports"
|
||||
},
|
||||
"about": {
|
||||
"label": "About"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,9 @@ export enum SettingsStringId {
|
||||
FirmwareTitle = 'settings.firmware.title',
|
||||
FirmwareCurrentProgramLabel = 'settings.firmware.flash-current-program.label',
|
||||
FirmwareCurrentProgramTooltip = 'settings.firmware.flash-current-program.tooltip',
|
||||
HelpTitle = 'settings.help.title',
|
||||
HelpSupportLabel = 'settings.help.support.label',
|
||||
HelpChatLabel = 'settings.help.chat.label',
|
||||
HelpBugsLabel = 'settings.help.bugs.label',
|
||||
HelpAboutLabel = 'settings.help.about.label',
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// Custom styling for the Settings* controls.
|
||||
|
||||
.pb-settings .bp3-form-group {
|
||||
margin: 25px;
|
||||
}
|
||||
|
||||
.pb-settings .bp3-label {
|
||||
font-size: 18px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.pb-settings .bp3-form-helper-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.bp3-progress-bar.status-bar-item {
|
||||
.#{$ns}-progress-bar.status-bar-item {
|
||||
// override progress bar default gray1 backgound
|
||||
background-color: $pt-app-background-color;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
// Custom styling for the Toolbar control.
|
||||
|
||||
.bp3-navbar-divider {
|
||||
@import '../variables.scss';
|
||||
|
||||
.#{$ns}-navbar-divider {
|
||||
// don't draw vertical line since we are just using button groups
|
||||
border-left: unset;
|
||||
}
|
||||
|
||||
@@ -36,3 +36,42 @@ body {
|
||||
.no-box-shadow {
|
||||
box-shadow: unset !important;
|
||||
}
|
||||
|
||||
// global style tweaks
|
||||
|
||||
.#{$ns}-form-group > .#{$ns}-label {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.#{$ns}-dark ::-webkit-scrollbar-track {
|
||||
background: $pt-dark-app-background-color;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: $pt-app-background-color;
|
||||
}
|
||||
|
||||
.#{$ns}-dark ::-webkit-scrollbar-thumb {
|
||||
border-color: $pt-dark-app-background-color;
|
||||
background: $pt-dark-icon-color;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-width: 3px;
|
||||
border-style: solid;
|
||||
border-radius: 8px;
|
||||
border-color: $pt-app-background-color;
|
||||
background: $pt-icon-color;
|
||||
}
|
||||
|
||||
.#{$ns}-dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: $pt-dark-icon-color-hover;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: $pt-icon-color-hover;
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
import { ResizeSensor } from '@blueprintjs/core';
|
||||
import { Classes, ResizeSensor } from '@blueprintjs/core';
|
||||
import { I18nContext, I18nManager } from '@shopify/react-i18n';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
@@ -10,7 +10,7 @@ import { applyMiddleware, createStore } from 'redux';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import './index.scss';
|
||||
import { success, update } from './actions/service-worker';
|
||||
import { didSucceed, didUpdate } from './actions/service-worker';
|
||||
import App from './components/App';
|
||||
import NotificationStack from './components/NotificationStack';
|
||||
import rootReducer from './reducers';
|
||||
@@ -38,9 +38,9 @@ store.subscribe(() => {
|
||||
const newDarkMode = store.getState().settings.darkMode;
|
||||
if (newDarkMode !== oldDarkMode) {
|
||||
if (newDarkMode) {
|
||||
document.body.classList.add('bp3-dark');
|
||||
document.body.classList.add(Classes.DARK);
|
||||
} else {
|
||||
document.body.classList.remove('bp3-dark');
|
||||
document.body.classList.remove(Classes.DARK);
|
||||
}
|
||||
oldDarkMode = newDarkMode;
|
||||
}
|
||||
@@ -76,8 +76,8 @@ ReactDOM.render(
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://cra.link/PWA
|
||||
serviceWorkerRegistration.register({
|
||||
onUpdate: (r) => store.dispatch(update(r)),
|
||||
onSuccess: (r) => store.dispatch(success(r)),
|
||||
onUpdate: (r) => store.dispatch(didUpdate(r)),
|
||||
onSuccess: (r) => store.dispatch(didSucceed(r)),
|
||||
});
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
||||
+28
-4
@@ -7,6 +7,12 @@ import { Reducer, combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { AppActionType } from '../actions/app';
|
||||
|
||||
export interface AppState {
|
||||
readonly showSettings: boolean;
|
||||
readonly showAboutDialog: boolean;
|
||||
readonly showLicenseDialog: boolean;
|
||||
}
|
||||
|
||||
const showSettings: Reducer<boolean, Action> = (state = false, action) => {
|
||||
switch (action.type) {
|
||||
case AppActionType.OpenSettings:
|
||||
@@ -18,8 +24,26 @@ const showSettings: Reducer<boolean, Action> = (state = false, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export interface AppState {
|
||||
readonly showSettings: boolean;
|
||||
}
|
||||
const showAboutDialog: Reducer<boolean, Action> = (state = false, action) => {
|
||||
switch (action.type) {
|
||||
case AppActionType.OpenAboutDialog:
|
||||
return true;
|
||||
case AppActionType.CloseAboutDialog:
|
||||
return false;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({ showSettings });
|
||||
const showLicenseDialog: Reducer<boolean, Action> = (state = false, action) => {
|
||||
switch (action.type) {
|
||||
case AppActionType.OpenLicenseDialog:
|
||||
return true;
|
||||
case AppActionType.CloseLicenseDialog:
|
||||
return false;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({ showSettings, showAboutDialog, showLicenseDialog });
|
||||
|
||||
@@ -7,6 +7,7 @@ import ble, { BleState } from './ble';
|
||||
import bootloader, { BootloaderState } from './bootloader';
|
||||
import editor, { EditorState } from './editor';
|
||||
import hub, { HubState } from './hub';
|
||||
import license, { LicenseState } from './license';
|
||||
import notification, { NotificationState } from './notification';
|
||||
import settings, { SettingsState } from './settings';
|
||||
import status, { StatusState } from './status';
|
||||
@@ -21,6 +22,7 @@ export interface RootState {
|
||||
readonly ble: BleState;
|
||||
readonly editor: EditorState;
|
||||
readonly hub: HubState;
|
||||
readonly license: LicenseState;
|
||||
readonly notification: NotificationState;
|
||||
readonly settings: SettingsState;
|
||||
readonly status: StatusState;
|
||||
@@ -33,6 +35,7 @@ export default combineReducers({
|
||||
ble,
|
||||
editor,
|
||||
hub,
|
||||
license,
|
||||
notification,
|
||||
settings,
|
||||
status,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { LicenseActionType } from '../actions/license';
|
||||
|
||||
export interface LicenseInfo {
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly author: string | undefined;
|
||||
readonly license: string;
|
||||
readonly licenseText: string;
|
||||
}
|
||||
|
||||
export type LicenseList = LicenseInfo[];
|
||||
|
||||
export interface LicenseState {
|
||||
readonly list: LicenseList | null;
|
||||
readonly selected: LicenseInfo | null;
|
||||
}
|
||||
|
||||
const list: Reducer<LicenseList | null, Action> = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case LicenseActionType.DidFetchList:
|
||||
return action.list;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const selected: Reducer<LicenseInfo | null, Action> = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case LicenseActionType.Select:
|
||||
return action.info;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default combineReducers({ list, selected });
|
||||
@@ -147,7 +147,7 @@ const list: Reducer<NotificationList, Action> = (state = [], action) => {
|
||||
];
|
||||
case NotificationActionType.Remove:
|
||||
return state.filter((e) => e.id !== action.id);
|
||||
case ServiceWorkerActionType.Update:
|
||||
case ServiceWorkerActionType.DidUpdate:
|
||||
return append(
|
||||
state,
|
||||
Level.Info,
|
||||
@@ -155,7 +155,7 @@ const list: Reducer<NotificationList, Action> = (state = [], action) => {
|
||||
undefined,
|
||||
'https://github.com/pybricks/pybricks-code/issues/102',
|
||||
);
|
||||
case ServiceWorkerActionType.Success:
|
||||
case ServiceWorkerActionType.DidSucceed:
|
||||
return append(state, Level.Info, MessageId.ServiceWorkerSuccess);
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Reducer, combineReducers } from 'redux';
|
||||
import { Action } from '../actions';
|
||||
import { SettingsActionType } from '../actions/settings';
|
||||
import { SettingId, getDefaultBooleanValue } from '../settings';
|
||||
import { SettingId, getDefaultBooleanValue } from '../settings/user';
|
||||
|
||||
export interface SettingsState {
|
||||
readonly darkMode: boolean;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
didFailToConnect as bleDidFailToConnect,
|
||||
} from '../actions/ble';
|
||||
import { didFailToWrite } from '../actions/ble-uart';
|
||||
import { didFailToFetchList } from '../actions/license';
|
||||
import {
|
||||
BootloaderConnectionFailureReason,
|
||||
didError,
|
||||
@@ -68,3 +69,17 @@ test('bootloaderDidError', async () => {
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('licenseDidFailToFetch', async () => {
|
||||
const saga = new AsyncSaga(errorLog);
|
||||
|
||||
console.error = jest.fn();
|
||||
saga.put(
|
||||
didFailToFetchList(
|
||||
new Response(undefined, { status: 404, statusText: 'not found' }),
|
||||
),
|
||||
);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
BleDeviceFailToConnectReasonType,
|
||||
} from '../actions/ble';
|
||||
import { BleUartActionType, BleUartDidFailToWriteAction } from '../actions/ble-uart';
|
||||
import { LicenseActionType, LicenseDidFailToFetchListAction } from '../actions/license';
|
||||
import {
|
||||
BootloaderConnectionActionType,
|
||||
BootloaderConnectionDidErrorAction,
|
||||
@@ -39,6 +40,10 @@ function bootloaderDidError(action: BootloaderConnectionDidErrorAction): void {
|
||||
console.error(action.err);
|
||||
}
|
||||
|
||||
function licenseDidFailToFetch(action: LicenseDidFailToFetchListAction): void {
|
||||
console.error(`Failed to fetch licenses: ${action.reason.statusText}`);
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(BleDeviceActionType.DidFailToConnect, bleDeviceDidFailToConnect);
|
||||
yield takeEvery(BleUartActionType.DidFailToWrite, bleDataDidFailToWrite);
|
||||
@@ -47,4 +52,5 @@ export default function* (): Generator {
|
||||
bootloaderDidFailToConnect,
|
||||
);
|
||||
yield takeEvery(BootloaderConnectionActionType.DidError, bootloaderDidError);
|
||||
yield takeEvery(LicenseActionType.DidFailToFetchList, licenseDidFailToFetch);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import editor from './editor';
|
||||
import errorLog from './error-log';
|
||||
import flashFirmware from './flash-firmware';
|
||||
import hub from './hub';
|
||||
import license from './license';
|
||||
import lwp3BootloaderBle from './lwp3-bootloader-ble';
|
||||
import lwp3BootloaderProtocol from './lwp3-bootloader-protocol';
|
||||
import mpy from './mpy';
|
||||
@@ -24,6 +25,7 @@ export default function* (): Generator {
|
||||
errorLog(),
|
||||
flashFirmware(),
|
||||
hub(),
|
||||
license(),
|
||||
mpy(),
|
||||
settings(),
|
||||
terminal(),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2021 The Pybricks Authors
|
||||
|
||||
// Tests for license sagas.
|
||||
|
||||
import { AsyncSaga, delay } from '../../test';
|
||||
import { openLicenseDialog } from '../actions/app';
|
||||
import { didFailToFetchList, didFetchList } from '../actions/license';
|
||||
import { LicenseList, LicenseState } from '../reducers/license';
|
||||
import license from './license';
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchLicenses', () => {
|
||||
test('first call', async () => {
|
||||
const testLicenseList: LicenseList = [];
|
||||
const saga = new AsyncSaga(license);
|
||||
|
||||
jest.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(testLicenseList)),
|
||||
);
|
||||
|
||||
// initially, license list starts as null, so fetch is called to get
|
||||
// the list
|
||||
saga.setState({ license: { list: null } as LicenseState });
|
||||
saga.put(openLicenseDialog());
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toEqual(didFetchList(testLicenseList));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
test('second call', async () => {
|
||||
const testLicenseList: LicenseList = [];
|
||||
const saga = new AsyncSaga(license);
|
||||
|
||||
jest.spyOn(globalThis, 'fetch').mockRejectedValue(
|
||||
'fetch () should not have been called',
|
||||
);
|
||||
|
||||
// after we have the list, we don't fetch it again since it will
|
||||
// always be the same list
|
||||
saga.setState({ license: { list: testLicenseList } as LicenseState });
|
||||
saga.put(openLicenseDialog());
|
||||
|
||||
// have to yield to be sure fetch call would have taken place on error
|
||||
await delay(0);
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
test('failed fetch', async () => {
|
||||
const failResponse = new Response(undefined, { status: 404 });
|
||||
const saga = new AsyncSaga(license);
|
||||
|
||||
jest.spyOn(globalThis, 'fetch').mockResolvedValue(failResponse);
|
||||
|
||||
saga.setState({ license: { list: null } as LicenseState });
|
||||
saga.put(openLicenseDialog());
|
||||
|
||||
const action = await saga.take();
|
||||
expect(action).toEqual(didFailToFetchList(failResponse));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { call, put, select, takeEvery } from 'redux-saga/effects';
|
||||
import { AppActionType } from '../actions/app';
|
||||
import { didFailToFetchList, didFetchList } from '../actions/license';
|
||||
import { RootState } from '../reducers';
|
||||
import { LicenseList } from '../reducers/license';
|
||||
|
||||
function* fetchLicenses(): Generator {
|
||||
const licenses = (yield select(
|
||||
(s: RootState) => s.license.list,
|
||||
)) as LicenseList | null;
|
||||
|
||||
// if we already have license list, nothing to do
|
||||
if (licenses !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = (yield call(() => fetch('static/oss-licenses.json'))) as Response;
|
||||
if (!response.ok || response.body === null) {
|
||||
yield put(didFailToFetchList(response));
|
||||
return;
|
||||
}
|
||||
|
||||
const list = (yield call(() => response.json())) as LicenseList;
|
||||
yield put(didFetchList(list));
|
||||
}
|
||||
|
||||
export default function* (): Generator {
|
||||
yield takeEvery(AppActionType.OpenLicenseDialog, fetchLicenses);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { AsyncSaga } from '../../test';
|
||||
import { startup } from '../actions/app';
|
||||
import { didBooleanChange, didFailToSetBoolean, setBoolean } from '../actions/settings';
|
||||
import { SettingsState } from '../reducers/settings';
|
||||
import { SettingId } from '../settings';
|
||||
import { SettingId } from '../settings/user';
|
||||
import settings from './settings';
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
didFailToSetBoolean,
|
||||
} from '../actions/settings';
|
||||
import { RootState } from '../reducers';
|
||||
import { SettingId, getDefaultBooleanValue } from '../settings';
|
||||
import { SettingId, getDefaultBooleanValue } from '../settings/user';
|
||||
|
||||
function stringToBoolean(value: string): boolean {
|
||||
return value.toLowerCase().match(/(true|yes|1)/) !== null;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// Definitions for compile-time UI settings.
|
||||
|
||||
/** Time in milliseconds before showing tooltip popover. */
|
||||
export const tooltipDelay = 1000;
|
||||
|
||||
/** Official name of the app. */
|
||||
export const appName = 'Pybricks Code';
|
||||
|
||||
/** URL to main Pybricks website. */
|
||||
export const pybricksWebsiteUrl = 'https://pybricks.com';
|
||||
|
||||
/** URL to Pybricks support site. */
|
||||
export const pybricksSupportUrl = 'https://github.com/pybricks/support/discussions';
|
||||
|
||||
/** URL to Pybricks bug report site. */
|
||||
export const pybricksBugReportsUrl = 'https://github.com/pybricks/support/issues';
|
||||
|
||||
/** URL for Pybricks community chat on Gitter */
|
||||
export const pybricksGitterUrl = 'https://gitter.im/pybricks/community';
|
||||
|
||||
/** Pybricks copyright statement. */
|
||||
export const pybricksCopyright = 'Copyright (c) 2020-2021 The Pybricks Authors';
|
||||
|
||||
// https://www.lego.com/en-us/legal/notices-and-policies/fair-play/
|
||||
/** LEGO "fair play" disclaimer */
|
||||
export const legoDisclaimer =
|
||||
'LEGO® is a trademark of the LEGO Group of companies which does not sponsor, authorize or endorse this site.';
|
||||
@@ -1,7 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
// Definitions for user settings.
|
||||
// Definitions for user selectable settings.
|
||||
|
||||
import { prefersDarkMode } from '../utils/os';
|
||||
|
||||
export enum SettingId {
|
||||
ShowDocs = 'showDocs',
|
||||
@@ -14,6 +16,7 @@ export function getDefaultBooleanValue(id: SettingId): boolean {
|
||||
case SettingId.ShowDocs:
|
||||
return window.innerWidth >= 1024;
|
||||
case SettingId.DarkMode:
|
||||
return prefersDarkMode();
|
||||
case SettingId.FlashCurrentProgram:
|
||||
return false;
|
||||
// istanbul ignore next: it is a programmer error if we hit this
|
||||
+20
-1
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
import { isMacOS } from './os';
|
||||
import { isMacOS, prefersDarkMode } from './os';
|
||||
|
||||
describe('isMacOS', () => {
|
||||
test('is true', () => {
|
||||
@@ -13,3 +13,22 @@ describe('isMacOS', () => {
|
||||
expect(isMacOS()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('prefersDarkMode', () => {
|
||||
test('is true', () => {
|
||||
window.matchMedia = jest.fn().mockReturnValue({
|
||||
matches: true,
|
||||
} as MediaQueryList);
|
||||
expect(prefersDarkMode()).toBeTruthy();
|
||||
});
|
||||
test('is false', () => {
|
||||
// @ts-expect-error 2790
|
||||
delete window.matchMedia;
|
||||
expect(prefersDarkMode()).toBeFalsy();
|
||||
|
||||
window.matchMedia = jest.fn().mockReturnValue({
|
||||
matches: false,
|
||||
} as MediaQueryList);
|
||||
expect(prefersDarkMode()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
|
||||
// Utility functions for dealing with operating systems.
|
||||
|
||||
/**
|
||||
* Tests if we are running on macOS.
|
||||
* @returns `true` if running on macOS, otherwise `false`.
|
||||
*/
|
||||
export function isMacOS(): boolean {
|
||||
return /mac/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the OS is set to dark mode.
|
||||
* @returns: `true` if dark mode should be preferred, otherwise `false`.
|
||||
*/
|
||||
export function prefersDarkMode(): boolean {
|
||||
if (!window.matchMedia) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
// override variables here
|
||||
// See https://blueprintjs.com/docs/#core/variables
|
||||
|
||||
$pt-font-size: $pt-grid-size * 1.6;
|
||||
$pt-font-size-large: $pt-grid-size * 1.8;
|
||||
$pt-font-size-small: $pt-grid-size * 1.4;
|
||||
|
||||
$pt-navbar-height: 72px;
|
||||
$pb-status-bar-height: 3vh;
|
||||
|
||||
|
||||
@@ -2101,6 +2101,15 @@
|
||||
"@types/source-list-map" "*"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@types/webpack-sources@^0.1.5":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81"
|
||||
integrity sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/source-list-map" "*"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@types/webpack@^4.41.8":
|
||||
version "4.41.25"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.25.tgz#4d3b5aecc4e44117b376280fbfd2dc36697968c4"
|
||||
@@ -2637,7 +2646,7 @@ arr-union@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
|
||||
integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
|
||||
|
||||
array-find-index@^1.0.1:
|
||||
array-find-index@^1.0.1, array-find-index@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
|
||||
integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
|
||||
@@ -7601,6 +7610,14 @@ levn@~0.3.0:
|
||||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
license-webpack-plugin@^2.3.11:
|
||||
version "2.3.11"
|
||||
resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz#0d93188a31fce350a44c86212badbaf33dcd29d8"
|
||||
integrity sha512-0iVGoX5vx0WDy8dmwTTpOOMYiGqILyUbDeVMFH52AjgBlS58lHwOlFMSoqg5nY8Kxl6+FRKyUZY/UdlQaOyqDw==
|
||||
dependencies:
|
||||
"@types/webpack-sources" "^0.1.5"
|
||||
webpack-sources "^1.2.0"
|
||||
|
||||
lie@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
|
||||
@@ -11144,6 +11161,15 @@ sourcemap-codec@^1.4.4:
|
||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||
|
||||
spdx-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/spdx-compare/-/spdx-compare-1.0.0.tgz#2c55f117362078d7409e6d7b08ce70a857cd3ed7"
|
||||
integrity sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==
|
||||
dependencies:
|
||||
array-find-index "^1.0.2"
|
||||
spdx-expression-parse "^3.0.0"
|
||||
spdx-ranges "^2.0.0"
|
||||
|
||||
spdx-correct@^3.0.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||
@@ -11170,6 +11196,20 @@ spdx-license-ids@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65"
|
||||
integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
|
||||
|
||||
spdx-ranges@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/spdx-ranges/-/spdx-ranges-2.1.1.tgz#87573927ba51e92b3f4550ab60bfc83dd07bac20"
|
||||
integrity sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==
|
||||
|
||||
spdx-satisfies@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/spdx-satisfies/-/spdx-satisfies-5.0.0.tgz#d740b8f14caeada36fb307629dee87146970a256"
|
||||
integrity sha512-/hGhwh20BeGmkA+P/lm06RvXD94JduwNxtx/oX3B5ClPt1/u/m5MCaDNo1tV3Y9laLkQr/NRde63b9lLMhlNfw==
|
||||
dependencies:
|
||||
spdx-compare "^1.0.0"
|
||||
spdx-expression-parse "^3.0.0"
|
||||
spdx-ranges "^2.0.0"
|
||||
|
||||
spdy-transport@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
|
||||
@@ -12418,7 +12458,7 @@ webpack-merge@^4.2.2:
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
|
||||
webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
|
||||
integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
|
||||
|
||||
Reference in New Issue
Block a user