182 lines
6.2 KiB
JavaScript
182 lines
6.2 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
let savedConfig = {};
|
|
let pendingChanges = {};
|
|
|
|
function setSavedConfig(config) {
|
|
savedConfig = config;
|
|
}
|
|
|
|
function populateFields(config) {
|
|
document.querySelectorAll('[data-key]').forEach(el => {
|
|
const key = el.dataset.key;
|
|
const value = config[key] || '';
|
|
if (el.type === 'checkbox') {
|
|
el.checked = value === 'true' || value === true;
|
|
} else if (el.type === 'color') {
|
|
const num = parseInt(value) || 0;
|
|
el.value = '#' + num.toString(16).padStart(6, '0');
|
|
} else {
|
|
el.value = value;
|
|
}
|
|
el.addEventListener('change', () => handleFieldChange(el, key));
|
|
el.addEventListener('input', () => {
|
|
if (el.type === 'text' || el.type === 'number' || el.type === 'password' || el.tagName === 'TEXTAREA') {
|
|
handleFieldChange(el, key);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function handleFieldChange(el, key) {
|
|
let value;
|
|
if (el.type === 'checkbox') {
|
|
value = el.checked ? 'true' : 'false';
|
|
} else if (el.type === 'color') {
|
|
value = '0x' + el.value.slice(1).toUpperCase();
|
|
} else {
|
|
value = el.value;
|
|
}
|
|
markChanged(key, value);
|
|
el.classList.toggle('changed', key in pendingChanges);
|
|
}
|
|
|
|
function initSmartSelects(config) {
|
|
document.querySelectorAll('[data-smart]').forEach(el => {
|
|
const key = el.dataset.key;
|
|
const type = el.dataset.smart;
|
|
const value = config[key] || '';
|
|
if (type === 'channel') DiscordFields.renderChannelSelect(el, value);
|
|
else if (type === 'category') DiscordFields.renderCategorySelect(el, value);
|
|
else if (type === 'role') DiscordFields.renderRoleSelect(el, value);
|
|
else if (type === 'member') DiscordFields.renderMemberSelect(el, value);
|
|
else if (type === 'multi-member') DiscordFields.renderMultiMemberSelect(el, value);
|
|
});
|
|
}
|
|
|
|
function markChanged(key, value) {
|
|
if (String(value) === String(savedConfig[key] || '')) {
|
|
delete pendingChanges[key];
|
|
} else {
|
|
pendingChanges[key] = value;
|
|
}
|
|
updateSaveBar();
|
|
}
|
|
|
|
function isChanged(key) {
|
|
return key in pendingChanges;
|
|
}
|
|
|
|
function updateSaveBar() {
|
|
const bar = document.getElementById('save-bar');
|
|
const count = Object.keys(pendingChanges).length;
|
|
bar.classList.toggle('visible', count > 0);
|
|
document.getElementById('change-count').textContent =
|
|
`${count} unsaved change${count !== 1 ? 's' : ''}`;
|
|
}
|
|
|
|
function setupSaveBar() {
|
|
updateSaveBar();
|
|
}
|
|
|
|
function clearFieldErrors() {
|
|
document.querySelectorAll('.field.field-error').forEach(f => f.classList.remove('field-error'));
|
|
document.querySelectorAll('.field-error-message').forEach(el => el.remove());
|
|
}
|
|
|
|
function normalizeErrorEntry(e) {
|
|
if (e && typeof e === 'object' && 'key' in e) {
|
|
return { key: e.key, message: e.error || 'Invalid value' };
|
|
}
|
|
// Tolerate legacy string shape "KEY: message" from older bot builds.
|
|
const str = String(e);
|
|
const idx = str.indexOf(':');
|
|
if (idx > 0) return { key: str.slice(0, idx).trim(), message: str.slice(idx + 1).trim() };
|
|
return { key: str, message: 'Invalid value' };
|
|
}
|
|
|
|
function applyFieldErrors(errors) {
|
|
let firstField = null;
|
|
for (const raw of errors) {
|
|
const { key, message } = normalizeErrorEntry(raw);
|
|
const selector = `[data-key="${(window.CSS && CSS.escape) ? CSS.escape(key) : key}"]`;
|
|
const input = document.querySelector(selector);
|
|
if (!input) continue;
|
|
const field = input.closest('.field');
|
|
if (!field) continue;
|
|
field.classList.add('field-error');
|
|
const msgEl = document.createElement('div');
|
|
msgEl.className = 'field-error-message';
|
|
msgEl.textContent = message;
|
|
msgEl.setAttribute('role', 'alert');
|
|
field.appendChild(msgEl);
|
|
if (!firstField) firstField = field;
|
|
}
|
|
if (firstField) firstField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
|
|
async function saveConfig(mode) {
|
|
const buttons = document.querySelectorAll('#save-bar button');
|
|
buttons.forEach(b => b.disabled = true);
|
|
clearFieldErrors();
|
|
try {
|
|
if (mode === 'restart' && !confirm('Save changes and restart the bot now?')) {
|
|
return;
|
|
}
|
|
const res = await fetch('/api/config', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: Util.csrfHeaders({ 'Content-Type': 'application/json' }),
|
|
body: JSON.stringify(pendingChanges)
|
|
});
|
|
const data = await res.json();
|
|
if (data.applied) {
|
|
for (const key of data.applied) savedConfig[key] = pendingChanges[key];
|
|
for (const key of data.applied) delete pendingChanges[key];
|
|
updateSaveBar();
|
|
document.querySelectorAll('.changed').forEach(el => {
|
|
const key = el.dataset && el.dataset.key;
|
|
if (!key || !(key in pendingChanges)) el.classList.remove('changed');
|
|
});
|
|
if (data.applied.length > 0) {
|
|
Util.showToast(`${data.applied.length} setting${data.applied.length === 1 ? '' : 's'} saved.`, 'success');
|
|
}
|
|
}
|
|
const hasErrors = data.errors && data.errors.length > 0;
|
|
if (hasErrors) {
|
|
applyFieldErrors(data.errors);
|
|
const keys = data.errors.map(e => normalizeErrorEntry(e).key).join(', ');
|
|
Util.showToast(`${data.errors.length} setting${data.errors.length === 1 ? '' : 's'} failed: ${keys}`, 'error');
|
|
}
|
|
if (mode === 'restart' && !hasErrors) {
|
|
await fetch('/api/restart', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: Util.csrfHeaders({ 'Content-Type': 'application/json' }),
|
|
body: JSON.stringify({ mode: 'immediate' })
|
|
});
|
|
Util.showToast('Restart initiated.', 'warning');
|
|
} else if (mode === 'restart' && hasErrors) {
|
|
Util.showToast('Restart cancelled — some settings failed validation.', 'warning');
|
|
}
|
|
} catch (e) {
|
|
Util.showToast('Failed to save. Bot may be unreachable.', 'error');
|
|
} finally {
|
|
buttons.forEach(b => b.disabled = false);
|
|
}
|
|
}
|
|
|
|
window.Fields = {
|
|
setSavedConfig,
|
|
populateFields,
|
|
handleFieldChange,
|
|
initSmartSelects,
|
|
markChanged,
|
|
isChanged,
|
|
updateSaveBar,
|
|
setupSaveBar,
|
|
saveConfig
|
|
};
|
|
})();
|