add dark mode setting

This commit is contained in:
David Lechner
2021-01-11 23:36:48 -06:00
parent 41a1cee092
commit d7dbaaa12d
12 changed files with 156 additions and 21 deletions
+2
View File
@@ -17,6 +17,7 @@ import {
import { MpyAction } from './mpy';
import { NotificationAction } from './notification';
import { ServiceWorkerAction } from './service-worker';
import { SettingsAction } from './settings';
import { TerminalDataAction } from './terminal';
/**
@@ -38,6 +39,7 @@ export type Action =
| MpyAction
| NotificationAction
| ServiceWorkerAction
| SettingsAction
| TerminalDataAction;
/**
+20
View File
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { Action } from 'redux';
/** Actions related to settings. */
export enum SettingsActionType {
ToggleDarkMode = 'settings.action.toggleDarkMode',
}
/** Action to toggle dark mode setting. */
export type SettingsToggleDarkModeAction = Action<SettingsActionType.ToggleDarkMode>;
/** Toggles dark mode setting on or off. */
export function toggleDarkMode(): SettingsToggleDarkModeAction {
return { type: SettingsActionType.ToggleDarkMode };
}
/** common type for all settings actions. */
export type SettingsAction = SettingsToggleDarkModeAction;
+15 -5
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import {
ContextMenuTarget,
@@ -16,10 +16,12 @@ import { IAceEditor } from 'react-ace/lib/types';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { setEditSession, storageChanged } from '../actions/editor';
import { RootState } from '../reducers';
import { EditorStringId } from './editor-i18n';
import en from './editor-i18n.en.json';
import 'ace-builds/src-noconflict/mode-python';
import 'ace-builds/src-noconflict/theme-tomorrow_night_eighties';
import 'ace-builds/src-noconflict/theme-xcode';
import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-keybinding_menu';
@@ -27,12 +29,16 @@ import 'ace-builds/src-noconflict/ext-language_tools';
import './editor-snippets';
type StateProps = {
darkMode: boolean;
};
type DispatchProps = {
onSessionChanged: (session?: Ace.EditSession) => void;
onProgramStorageChanged: (newValue: string) => void;
};
type EditorProps = DispatchProps & WithI18nProps;
type EditorProps = StateProps & DispatchProps & WithI18nProps;
@ContextMenuTarget
class Editor extends React.Component<EditorProps> {
@@ -68,14 +74,14 @@ class Editor extends React.Component<EditorProps> {
}
render(): JSX.Element {
const { i18n, onSessionChanged } = this.props;
const { darkMode, i18n, onSessionChanged } = this.props;
return (
<div className="h-100 watermark">
<ResizeSensor onResize={(): void => this.editor?.resize()}>
<AceEditor
ref={this.editorRef}
mode="python"
theme="xcode"
theme={darkMode ? 'tomorrow_night_eighties' : 'xcode'}
fontSize="16pt"
width="100%"
height="100%"
@@ -171,12 +177,16 @@ class Editor extends React.Component<EditorProps> {
}
}
const mapStateToProps = (state: RootState): StateProps => ({
darkMode: state.settings.darkMode,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onSessionChanged: (s): Action => dispatch(setEditSession(s)),
onProgramStorageChanged: (v): Action => dispatch(storageChanged(v)),
});
export default connect(
undefined,
mapStateToProps,
mapDispatchToProps,
)(withI18n({ id: 'editor', fallback: en, translations: { en } })(Editor));
+23 -3
View File
@@ -1,46 +1,66 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { Drawer } from '@blueprintjs/core';
import { Drawer, FormGroup, Switch } 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 { toggleDarkMode } from '../actions/settings';
import { RootState } from '../reducers';
import { SettingsStringId } from './settings-i18n';
import en from './settings-i18n.en.json';
import './settings.scss';
type StateProps = {
open: boolean;
darkMode: boolean;
};
type DispatchProps = {
onClose: () => void;
onDarkModeChanged: () => void;
};
type SettingsProps = StateProps & DispatchProps & WithI18nProps;
class SettingsDrawer extends React.PureComponent<SettingsProps> {
render(): JSX.Element {
const { i18n, open, onClose } = this.props;
const { i18n, open, onClose, darkMode, onDarkModeChanged } = this.props;
return (
<Drawer
isOpen={open}
icon="cog"
title={i18n.translate(SettingsStringId.Title)}
onClose={() => onClose()}
></Drawer>
>
<div className="pb-settings">
<FormGroup label={i18n.translate(SettingsStringId.AppearanceTitle)}>
<Switch
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
)}
large={true}
checked={darkMode}
onChange={() => onDarkModeChanged()}
/>
</FormGroup>
</div>
</Drawer>
);
}
}
const mapStateToProps = (state: RootState): StateProps => ({
open: state.app.showSettings,
darkMode: state.settings.darkMode,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onClose: (): Action => dispatch(closeSettings()),
onDarkModeChanged: (): Action => dispatch(toggleDarkMode()),
});
export default connect(
+12 -8
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import {
ContextMenuTarget,
@@ -24,6 +24,7 @@ import 'xterm/css/xterm.css';
interface StateProps {
dataSource: Observable<string> | null;
darkMode: boolean;
}
interface DispatchProps {
@@ -45,13 +46,6 @@ class Terminal extends React.Component<TerminalProps> {
cursorBlink: true,
cursorStyle: 'underline',
fontSize: 18,
theme: {
background: 'white',
foreground: 'black',
cursor: 'black',
// transparency is needed to work around https://github.com/xtermjs/xterm.js/issues/2808
selection: 'rgba(181,213,255,0.5)', // this should match AceEditor theme
},
});
this.fitAddon = new FitAddon();
this.xterm.loadAddon(this.fitAddon);
@@ -112,6 +106,15 @@ class Terminal extends React.Component<TerminalProps> {
}
render(): JSX.Element {
this.xterm.setOption('theme', {
background: this.props.darkMode ? 'black' : 'white',
foreground: this.props.darkMode ? 'white' : 'black',
cursor: this.props.darkMode ? 'white' : 'black',
// transparency is needed to work around https://github.com/xtermjs/xterm.js/issues/2808
selection: this.props.darkMode
? 'rgb(81,81,81,0.5)'
: 'rgba(181,213,255,0.5)', // this should match AceEditor theme
});
return (
<div className="h-100">
<ResizeSensor onResize={(): void => this.fitAddon.fit()}>
@@ -163,6 +166,7 @@ class Terminal extends React.Component<TerminalProps> {
const mapStateToProps = (state: RootState): StateProps => ({
dataSource: state.terminal.dataSource,
darkMode: state.settings.darkMode,
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
+7 -1
View File
@@ -1,5 +1,11 @@
{
"settings": {
"title": "Settings"
"title": "Settings",
"appearance": {
"title": "Appearance",
"dark-mode": {
"label": "Dark mode"
}
}
}
}
+2
View File
@@ -5,4 +5,6 @@
export enum SettingsStringId {
Title = 'settings.title',
AppearanceTitle = 'settings.appearance.title',
AppearanceDarkModeLabel = 'settings.appearance.dark-mode.label',
}
+6
View File
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
.pb-settings {
margin: 25px;
}
+15 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
@import '@blueprintjs/core/lib/scss/variables.scss';
@@ -82,16 +82,30 @@ body {
border-left: unset;
}
.bp3-dark .ace_gutter {
// make ace editor match app backgound color
background-color: $pt-dark-app-background-color !important;
}
.ace_gutter {
// make ace editor match app backgound color
background-color: $pt-app-background-color !important;
}
.bp3-dark .splitter-layout > .layout-splitter {
// make layout splitter match app color scheme
background-color: $pt-dark-app-background-color !important;
}
.layout-splitter {
// make layout splitter match app color scheme
background-color: $pt-app-background-color !important;
}
.bp3-dark .terminal-padding {
background-color: black;
}
.terminal-padding {
padding-left: 10px;
}
+14 -3
View File
@@ -1,10 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { combineReducers } from 'redux';
import { Reducer, combineReducers } from 'redux';
import { Action } from '../actions';
import { SettingsActionType } from '../actions/settings';
export interface SettingsState {
readonly visible: boolean;
readonly darkMode: boolean;
}
export default combineReducers({});
const darkMode: Reducer<boolean, Action> = (state = false, action) => {
switch (action.type) {
case SettingsActionType.ToggleDarkMode:
return !state;
default:
return state;
}
};
export default combineReducers({ darkMode });
+2
View File
@@ -12,6 +12,7 @@ import hub from './hub';
import lwp3BootloaderBle from './lwp3-bootloader-ble';
import lwp3BootloaderProtocol from './lwp3-bootloader-protocol';
import mpy from './mpy';
import settings from './settings';
import terminal from './terminal';
/* istanbul ignore next */
@@ -26,6 +27,7 @@ export default function* (): Generator {
flashFirmware(),
hub(),
mpy(),
settings(),
terminal(),
put(startup()),
]);
+38
View File
@@ -0,0 +1,38 @@
import { put, select, takeEvery } from 'redux-saga/effects';
import { AppActionType } from '../actions/app';
import { SettingsActionType, toggleDarkMode } from '../actions/settings';
import { RootState } from '../reducers';
import { SettingsState } from '../reducers/settings';
function* loadSettings(): Generator {
const settingsString = localStorage.getItem('settings');
if (!settingsString) {
return;
}
const settings = JSON.parse(settingsString) as SettingsState;
// TODO: there has to be a better way to initialize app state from settings
if (settings.darkMode) {
yield put(toggleDarkMode());
}
}
function* saveSettings(): Generator {
const settings = (yield select((s: RootState) => s.settings)) as SettingsState;
localStorage.setItem('settings', JSON.stringify(settings));
}
function* updateDarkModeClass(): Generator {
const darkMode = (yield select((s: RootState) => s.settings.darkMode)) as boolean;
if (darkMode) {
document.body.classList.add('bp3-dark');
} else {
document.body.classList.remove('bp3-dark');
}
}
export default function* (): Generator {
yield takeEvery(AppActionType.Startup, loadSettings);
yield takeEvery(Object.values(SettingsActionType), saveSettings);
yield takeEvery(SettingsActionType.ToggleDarkMode, updateDarkModeClass);
}