mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
add dark mode setting
This commit is contained in:
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "Settings"
|
||||
"title": "Settings",
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"dark-mode": {
|
||||
"label": "Dark mode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,6 @@
|
||||
|
||||
export enum SettingsStringId {
|
||||
Title = 'settings.title',
|
||||
AppearanceTitle = 'settings.appearance.title',
|
||||
AppearanceDarkModeLabel = 'settings.appearance.dark-mode.label',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2021 The Pybricks Authors
|
||||
|
||||
.pb-settings {
|
||||
margin: 25px;
|
||||
}
|
||||
+15
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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()),
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user