Skip to content

Commit bc7338f

Browse files
authored
Merge pull request #442 from makermelissa/beta
Add settings and theme options
2 parents d289acf + fb6f068 commit bc7338f

9 files changed

Lines changed: 404 additions & 138 deletions

File tree

index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
<button id="btn-mode-editor" class="mode-button active">Editor</button>
121121
<button id="btn-mode-serial" class="mode-button">Serial</button>
122122
<div class="spacer"></div>
123+
<button class="purple-button btn-settings">Settings<i class="fa-solid fa-gear"></i></button>
123124
<button class="purple-button btn-info" disabled>Info<i class="fa-solid fa-info-circle"></i></button>
124125
</div>
125126
</div>
@@ -153,6 +154,14 @@
153154
<button class="purple-button cancel-button">Cancel</button>
154155
</div>
155156
</div>
157+
<div class="popup-modal shadow settings-dialog closable" data-popup-modal="settings">
158+
<i class="fa-solid fa-2x fa-xmark text-white bg-primary p-3 popup-modal__close"></i>
159+
<div id="settings-content"></div>
160+
<div class="buttons centered">
161+
<button class="purple-button ok-button">Save</button>
162+
<button class="purple-button cancel-button">Cancel</button>
163+
</div>
164+
</div>
156165
<div class="popup-modal shadow file-dialog closable" data-popup-modal="folder-select">
157166
<span id="current-path"></span>
158167
<i class="fa-solid fa-2x fa-xmark text-white bg-primary p-3 popup-modal__close"></i>

js/common/settings.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {GenericModal} from './dialogs.js';
2+
3+
class SettingsDialog extends GenericModal {
4+
constructor(modalId, settingsData) {
5+
super(modalId);
6+
this._settingsData = settingsData;
7+
this._settings = {}
8+
}
9+
10+
async open(settings) {
11+
let p = super.open();
12+
const cancelButton = this._currentModal.querySelector("button.cancel-button");
13+
this._addDialogElement('cancelButton', cancelButton, 'click', this._closeModal);
14+
const okButton = this._currentModal.querySelector("button.ok-button");
15+
this._addDialogElement('okButton', okButton, 'click', this._handleOkButton);
16+
17+
const contentDiv = this._currentModal.querySelector("#settings-content");
18+
contentDiv.innerHTML = '';
19+
20+
for (const setting of this._settingsData) {
21+
const label = document.createElement('label');
22+
label.textContent = setting.label;
23+
if (setting.icon) {
24+
const icon = document.createElement('i');
25+
icon.className = `fa-solid fa-${setting.icon} setting-item-icon`;
26+
label.prepend(icon);
27+
}
28+
label.htmlFor = `setting-${setting.key}`;
29+
contentDiv.appendChild(label);
30+
31+
const control = await this._createControl(setting);
32+
control.value = settings[setting.key];
33+
contentDiv.appendChild(control);
34+
}
35+
36+
return p;
37+
}
38+
39+
async _handleOkButton() {
40+
let settings = {}
41+
for (const setting of this._settingsData) {
42+
const control = this._currentModal.querySelector(`#setting-${setting.key}`);
43+
settings[setting.key] = control.value;
44+
}
45+
this._returnValue(settings);
46+
}
47+
48+
async _createControl(settingData) {
49+
// Return the created control
50+
let control;
51+
if (settingData.type === 'select') {
52+
control = document.createElement('select');
53+
for (const optionValue of settingData.options) {
54+
const option = document.createElement('option');
55+
option.value = optionValue;
56+
option.textContent = optionValue.charAt(0).toUpperCase() + optionValue.slice(1);
57+
control.appendChild(option);
58+
}
59+
}
60+
control.id = `setting-${settingData.key}`;
61+
62+
// this will also call this._addDialogElement to add event listeners as needed
63+
this._addDialogElement(`setting-${settingData.key}`, control);
64+
return control;
65+
}
66+
}
67+
68+
class Settings {
69+
// This is a class that handles loading/saving settings as well as providing a settings dialog
70+
constructor() {
71+
// This will hold the layout/save data for the settings
72+
this._settingsData = [
73+
{ key: 'theme', type: 'select', label: 'Editor Theme', icon: 'palette', options: ['dark', 'light'], default: 'dark' }
74+
];
75+
this._settings = {};
76+
this._loadSettings();
77+
78+
this._settingsDialog = new SettingsDialog('settings', this._settingsData);
79+
}
80+
81+
_loadSettings() {
82+
// Load all saved settings or defaults
83+
for (const setting of this._settingsData) {
84+
this._settings[setting.key] = this._loadSetting(setting.key, setting.default);
85+
}
86+
}
87+
88+
_saveSettings() {
89+
// Save all settings
90+
for (const key in this._settings) {
91+
this._saveSetting(key, this._settings[key]);
92+
}
93+
}
94+
95+
_loadSetting(setting, defaultValue) {
96+
let value = JSON.parse(window.localStorage.getItem(setting));
97+
if (value == null) {
98+
return defaultValue;
99+
}
100+
101+
return value;
102+
}
103+
104+
_saveSetting(setting, value) {
105+
window.localStorage.setItem(setting, JSON.stringify(value));
106+
}
107+
108+
getSetting(key) {
109+
return this._settings[key];
110+
}
111+
112+
async showDialog() {
113+
this._settings = await this._settingsDialog.open(this._settings);
114+
if (this._settings) {
115+
this._saveSettings();
116+
return true;
117+
}
118+
return false;
119+
}
120+
}
121+
122+
123+
export {
124+
Settings
125+
};

js/script.js

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { basicSetup } from "codemirror";
22
import { EditorView, keymap } from "@codemirror/view";
33
import { EditorState } from "@codemirror/state";
4-
import {indentWithTab} from "@codemirror/commands"
4+
import { indentWithTab } from "@codemirror/commands"
55
import { python } from "@codemirror/lang-python";
66
import { syntaxHighlighting, indentUnit } from "@codemirror/language";
77
import { classHighlighter } from "@lezer/highlight";
@@ -17,9 +17,10 @@ import { WebWorkflow } from './workflows/web.js';
1717
import { isValidBackend, getBackendWorkflow, getWorkflowBackendName } from './workflows/workflow.js';
1818
import { ButtonValueDialog, MessageModal } from './common/dialogs.js';
1919
import { isLocal, switchUrl, getUrlParam } from './common/utilities.js';
20+
import { Settings } from './common/settings.js';
2021
import { CONNTYPE } from './constants.js';
2122
import './layout.js'; // load for side effects only
22-
import {setupPlotterChart} from "./common/plotter.js";
23+
import { setupPlotterChart } from "./common/plotter.js";
2324
import { mainContent, showSerial } from './layout.js';
2425

2526
// Instantiate workflows
@@ -31,6 +32,7 @@ workflows[CONNTYPE.Web] = new WebWorkflow();
3132
let workflow = null;
3233
let unchanged = 0;
3334
let connectionPromise = null;
35+
let debugMessageAnsi = null;
3436

3537
const btnRestart = document.querySelector('.btn-restart');
3638
const btnHalt = document.querySelector('.btn-halt');
@@ -43,13 +45,15 @@ const btnSave = document.querySelectorAll('.btn-save');
4345
const btnSaveAs = document.querySelectorAll('.btn-save-as');
4446
const btnSaveRun = document.querySelectorAll('.btn-save-run');
4547
const btnInfo = document.querySelector('.btn-info');
48+
const btnSettings = document.querySelector('.btn-settings');
4649
const terminalTitle = document.getElementById('terminal-title');
4750
const serialPlotter = document.getElementById('plotter');
4851

4952
const messageDialog = new MessageModal("message");
5053
const connectionType = new ButtonValueDialog("connection-type");
54+
const settings = new Settings();
5155

52-
const editorTheme = EditorView.theme({}, {dark: true});
56+
const editorTheme = EditorView.theme({}, {dark: getCssVar('editor-theme-dark').trim() === '1'});
5357

5458
document.addEventListener('DOMContentLoaded', function() {
5559
document.getElementById('mobile-menu-button').addEventListener('click', handleMobileToggle);
@@ -168,6 +172,12 @@ btnInfo.addEventListener('click', async function(e) {
168172
}
169173
});
170174

175+
btnSettings.addEventListener('click', async function(e) {
176+
if (await settings.showDialog()) {
177+
applySettings();
178+
}
179+
});
180+
171181
// Basic functions used for buttons and hotkeys
172182
async function openFile() {
173183
if (await checkConnected()) {
@@ -420,8 +430,12 @@ async function showMessage(message) {
420430
}
421431

422432
async function debugLog(msg) {
433+
if (debugMessageAnsi === null) {
434+
const colorCode = getCssVar('debug-message-color').trim();
435+
debugMessageAnsi = `\x1b[38;2;${parseInt(colorCode.slice(1,3),16)};${parseInt(colorCode.slice(3,5),16)};${parseInt(colorCode.slice(5,7),16)}m`;
436+
}
423437
state.terminal.writeln(''); // get a fresh line without any prior content (a '>>>' prompt might be there without newline)
424-
state.terminal.writeln(`\x1b[93m${msg}\x1b[0m`);
438+
state.terminal.writeln(`${debugMessageAnsi}${msg}\x1b[0m`);
425439
}
426440

427441
function updateUIConnected(isConnected) {
@@ -547,12 +561,16 @@ editor = new EditorView({
547561
parent: document.querySelector('#editor')
548562
});
549563

564+
function getCssVar(varName) {
565+
return window.getComputedStyle(document.body).getPropertyValue("--" + varName);
566+
}
567+
550568
async function setupXterm() {
551569
state.terminal = new Terminal({
552570
theme: {
553-
background: '#333',
554-
foreground: '#ddd',
555-
cursor: '#ddd',
571+
background: getCssVar('background-color'),
572+
foreground: getCssVar('terminal-text-color'),
573+
cursor: getCssVar('terminal-text-color'),
556574
}
557575
});
558576

@@ -585,8 +603,40 @@ function loadParameterizedContent() {
585603
return documentState;
586604
}
587605

606+
function applySettings() {
607+
// ----- Themes -----
608+
const theme = settings.getSetting('theme');
609+
// Remove all theme-[option] classes from body
610+
document.body.classList.forEach((className) => {
611+
if (className.startsWith('theme-')) {
612+
document.body.classList.remove(className);
613+
}
614+
});
615+
616+
// Add the selected theme class
617+
document.body.classList.add(`theme-${theme}`);
618+
619+
// Apply to EditorView.theme dark parameter
620+
editor.darkTheme = getCssVar('editor-theme-dark').trim() === '1';
621+
622+
// Apply to xterm
623+
state.terminal.options.theme = {
624+
background: getCssVar('background-color'),
625+
foreground: getCssVar('terminal-text-color'),
626+
cursor: getCssVar('terminal-text-color'),
627+
};
628+
629+
debugMessageAnsi = null;
630+
631+
// Note: Debug Message color is applied on next debug message or reload
632+
// I'm not sure how to go through the xterm's existing content and change escape sequences
633+
// Changing the CSS style reverts to the old style on terminal update/redraw
634+
635+
}
636+
588637
document.addEventListener('DOMContentLoaded', async (event) => {
589638
await setupXterm();
639+
applySettings();
590640
btnConnect.forEach((element) => {
591641
element.addEventListener('click', async function(e) {
592642
e.preventDefault();

package-lock.json

Lines changed: 13 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
"chart.js": "^4.5.1",
2525
"codemirror": "^6.0.2",
2626
"file-saver": "^2.0.5",
27-
"focus-trap": "^7.7.0",
27+
"focus-trap": "^7.7.1",
2828
"idb-keyval": "^6.2.2",
2929
"jszip": "^3.10.1"
3030
},
3131
"optionalDependencies": {
32-
"@rollup/rollup-linux-x64-gnu": "^4.54.0"
32+
"@rollup/rollup-linux-x64-gnu": "^4.55.1"
3333
}
3434
}

0 commit comments

Comments
 (0)