feat: config frontend

Implement nicer configuration UI:

- Show human-readable names for config settings
- Show error message received from server if storing settings fails
- Show appropriate input elements for enum choice and numerical inputs

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
This commit is contained in:
2025-12-14 14:01:35 +01:00
parent aee5a48967
commit 93ea5036dc

View File

@@ -111,13 +111,48 @@ function renderConfigForm(config) {
document.getElementById('save-btn').disabled = false;
}
const config_names = {
'root.IDLE_TIMEOUT_SECS': 'Idle Timeout (seconds)',
'root.BUTTON_MAP': 'Button map',
'root.BUTTON_MAP.NEXT': 'Next track',
'root.BUTTON_MAP.PREV': 'Previous track',
'root.BUTTON_MAP.VOLUP': 'Volume up',
'root.BUTTON_MAP.VOLDOWN': 'Volume down',
'root.BUTTON_MAP.PLAY_PAUSE': 'Play/Pause',
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
'root.TAGMODE': 'Tag mode',
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain'
};
const config_input_override = {
'root.TAGMODE': {
'element': 'select',
'values': {
'tagremains': 'Play until tag is removed',
'tagstartstop': 'Present tag once to start, present again to stop playback'
}
},
'root.IDLE_TIMEOUT_SECS': {
'input-type': 'number'
},
'root.TAG_TIMEOUT_SECS': {
'input-type': 'number'
},
'root.LED_COUNT': {
'input-type': 'number'
},
};
function renderObject(obj, path) {
const wrapper = document.createElement('div');
Object.entries(obj).forEach(([key, value]) => {
const currentPath = path + '.' + key;
const label = document.createElement('label');
label.textContent = key;
if (currentPath in config_names) {
label.textContent = config_names[currentPath];
} else {
label.textContent = key;
}
if (value !== null && typeof value === 'object') {
wrapper.appendChild(label);
@@ -126,12 +161,36 @@ function renderObject(obj, path) {
nested.appendChild(renderObject(value, currentPath));
wrapper.appendChild(nested);
} else {
const input = document.createElement('input');
input.value = value === null ? "" : value;
input.dataset.path = currentPath;
wrapper.appendChild(label);
wrapper.appendChild(input);
if (currentPath in config_input_override && 'element' in config_input_override[currentPath]) {
const override = config_input_override[currentPath];
if (override['element'] === 'select') {
const input = document.createElement('select');
input.dataset.path = currentPath;
for (const val in override.values) {
const option = document.createElement('option');
option.value = val;
option.textContent = override.values[val];
if (val === value) {
option.selected = true;
}
input.appendChild(option);
}
wrapper.appendChild(input);
}
} else {
const input = document.createElement('input');
if (currentPath in config_input_override && 'input-type' in config_input_override[currentPath]) {
input.type = config_input_override[currentPath]['input-type'];
}
input.value = value === null ? "" : value;
input.dataset.path = currentPath;
wrapper.appendChild(input);
}
}
});
@@ -139,7 +198,7 @@ function renderObject(obj, path) {
}
function serializeConfig(rootObj) {
const inputs = document.querySelectorAll("input[data-path]");
const inputs = document.querySelectorAll("input[data-path], select[data-path]");
inputs.forEach(input => {
const path = input.dataset.path.split('.').slice(1); // remove "root"
@@ -172,7 +231,7 @@ document.getElementById('save-btn').addEventListener('click', async () => {
});
if (!saveRes.ok) {
alert("Failed to save config!");
alert("Failed to save config: " + await saveRes.text());
return;
}