Files

327 lines
11 KiB
JavaScript

/* Settings page — sovereign://settings
*
* Fetches all settings values from sovereign://settings/config (JSON) on
* page load and populates the DOM. Toggles, saves, and shortcut capture
* use the existing sovereign://settings/set endpoint.
*/
function sovereignGet(url) {
return new Promise(function(resolve, reject) {
var x = new XMLHttpRequest();
x.open('GET', url, true);
x.onreadystatechange = function() {
if (x.readyState !== 4) return;
try { resolve(JSON.parse(x.responseText)); }
catch (e) { reject(e); }
};
x.onerror = function() { reject(new Error('XHR error')); };
x.send();
});
}
function esc(s) {
var d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
function showStatus(cls, msg) {
var s = document.getElementById('status');
s.className = 'status ' + cls + ' show';
s.textContent = msg;
}
/* Set a toggle element's on/off class. */
function setToggleClass(el, on) {
el.classList.remove('on', 'off');
el.classList.add(on ? 'on' : 'off');
}
/* Apply the settings/config JSON to the DOM. */
function applyConfig(d) {
/* Toggles — each has a data-feature attribute identifying the key. */
var toggleMap = {
theme_dark: d.theme_dark,
restore_session: d.restore_session,
show_tab_close_buttons: d.show_tab_close_buttons,
middle_click_close: d.middle_click_close,
ctrl_tab_switch: d.ctrl_tab_switch,
tab_drag_reorder: d.tab_drag_reorder,
agent_server_enabled: d.agent_server_enabled,
dev_extras: d.dev_extras,
file_access: d.file_access,
universal_access: d.universal_access
};
document.querySelectorAll('.toggle[data-feature]').forEach(function(el) {
var key = el.getAttribute('data-feature');
if (key === 'theme_dark') el.id = 'toggle_theme_dark';
setToggleClass(el, !!toggleMap[key]);
});
/* Text/number inputs. */
if (d.new_tab_url != null)
document.getElementById('new_tab_url').value = d.new_tab_url;
if (d.max_tabs != null)
document.getElementById('max_tabs').value = d.max_tabs;
if (d.agent_server_port != null)
document.getElementById('agent_server_port').value = d.agent_server_port;
if (d.agent_allowed_origins != null)
document.getElementById('agent_allowed_origins').value = d.agent_allowed_origins;
if (d.agent_login_timeout_ms != null)
document.getElementById('agent_login_timeout_ms').value = d.agent_login_timeout_ms;
if (d.bootstrap_relays != null)
document.getElementById('bootstrap_relays').value = d.bootstrap_relays;
/* Tab bar position select. */
if (d.tab_bar_position != null) {
var sel = document.getElementById('tab_bar_position');
sel.value = d.tab_bar_position;
}
/* Search engine select. */
var se = document.getElementById('search_engine');
se.innerHTML = '';
(d.search_engines || []).forEach(function(eng) {
var opt = document.createElement('option');
opt.value = eng.id;
opt.textContent = eng.name;
if (eng.id === d.search_engine) opt.selected = true;
se.appendChild(opt);
});
/* Keyboard shortcuts. */
renderShortcuts(d.shortcuts || []);
}
/* ── Keyboard shortcut capture ──────────────────────────────────── */
var capturingAction = null;
var capturingDisplay = null;
function jsKeyToGdk(e) {
var k = e.key;
if (k.length === 1) {
var c = k.toLowerCase();
if (c >= 'a' && c <= 'z') return c;
if (c >= '0' && c <= '9') return c;
if (k === ',') return 'comma';
if (k === ' ') return 'space';
if (k === '.') return 'period';
if (k === '/') return 'slash';
if (k === '-') return 'minus';
if (k === '=') return 'equal';
if (k === '+') return 'plus';
}
var special = {
'Tab':'Tab','Enter':'Return','Escape':'Escape',
'Backspace':'BackSpace','Delete':'Delete',
'Home':'Home','End':'End',
'PageUp':'Page_Up','PageDown':'Page_Down',
'ArrowLeft':'Left','ArrowRight':'Right',
'ArrowUp':'Up','ArrowDown':'Down',
'F1':'F1','F2':'F2','F3':'F3','F4':'F4','F5':'F5','F6':'F6',
'F7':'F7','F8':'F8','F9':'F9','F10':'F10','F11':'F11','F12':'F12'
};
if (special[k]) return special[k];
return null;
}
function buildAccel(e) {
var parts = [];
if (e.ctrlKey) parts.push('<Control>');
if (e.altKey) parts.push('<Alt>');
if (e.shiftKey) parts.push('<Shift>');
if (e.metaKey) parts.push('<Super>');
var gdkKey = jsKeyToGdk(e);
if (!gdkKey) return null;
parts.push(gdkKey);
return parts.join('');
}
function displayAccel(accel) {
return accel.replace(/<Control>/g,'Ctrl+')
.replace(/<Alt>/g,'Alt+')
.replace(/<Shift>/g,'Shift+')
.replace(/<Super>/g,'Super+');
}
function renderShortcuts(shortcuts) {
var c = document.getElementById('shortcuts-section');
var html = '';
shortcuts.forEach(function(sc) {
var accel = sc.accel || '';
html += '<div class="field shortcut-row" data-action="' + esc(sc.id) +
'" data-accel="' + esc(accel) + '">';
html += '<div><div class="setting-name">' + esc(sc.label) + '</div></div>';
html += '<div><span class="shortcut-display" id="sc_' + esc(sc.id) + '">' +
esc(displayAccel(accel)) + '</span>';
html += ' <button class="save-btn sc-change" data-action="' + esc(sc.id) +
'">Change</button>';
html += ' <button class="save-btn sc-reset" data-action="' + esc(sc.id) +
'">Reset</button></div>';
html += '</div>';
});
c.innerHTML = html;
bindShortcutButtons();
checkDuplicates();
}
function checkDuplicates() {
var rows = document.querySelectorAll('.shortcut-row');
var seen = {};
rows.forEach(function(r) {
r.classList.remove('duplicate');
var a = r.getAttribute('data-accel');
if (a) {
if (seen[a]) seen[a].push(r);
else seen[a] = [r];
}
});
Object.keys(seen).forEach(function(a) {
if (seen[a].length > 1) {
seen[a].forEach(function(r) { r.classList.add('duplicate'); });
}
});
}
function bindShortcutButtons() {
document.querySelectorAll('.sc-change').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = btn.getAttribute('data-action');
var disp = document.getElementById('sc_' + action);
if (!disp) return;
if (capturingDisplay) capturingDisplay.classList.remove('capturing');
capturingAction = action;
capturingDisplay = disp;
disp.setAttribute('data-prev', disp.textContent);
disp.classList.add('capturing');
disp.textContent = 'Press keys…';
});
});
document.querySelectorAll('.sc-reset').forEach(function(btn) {
btn.addEventListener('click', function() {
resetShortcut(btn.getAttribute('data-action'));
});
});
var ra = document.getElementById('sc-reset-all');
if (ra) ra.onclick = resetAllShortcuts;
}
function commitShortcut(action, accel) {
sovereignGet('sovereign://settings/set?key=shortcut.' + action +
'&value=' + encodeURIComponent(accel))
.then(function(d) {
if (d.error) showStatus('err', d.message);
else {
showStatus('ok', action + ' = ' + displayAccel(accel));
setTimeout(function() { location.reload(); }, 400);
}
})
.catch(function(e) { showStatus('err', 'Error: ' + e.message); });
}
function resetShortcut(action) {
sovereignGet('sovereign://settings/set?key=shortcut.reset&action=' + action)
.then(function(d) {
if (d.error) showStatus('err', d.message);
else {
showStatus('ok', action + ' reset to default');
setTimeout(function() { location.reload(); }, 400);
}
})
.catch(function(e) { showStatus('err', 'Error: ' + e.message); });
}
function resetAllShortcuts() {
sovereignGet('sovereign://settings/set?key=shortcut.reset_all')
.then(function(d) {
if (d.error) showStatus('err', d.message);
else {
showStatus('ok', 'All shortcuts reset to defaults');
setTimeout(function() { location.reload(); }, 400);
}
})
.catch(function(e) { showStatus('err', 'Error: ' + e.message); });
}
document.addEventListener('keydown', function(e) {
if (!capturingAction) return;
e.preventDefault();
e.stopPropagation();
if (e.key === 'Escape') {
capturingDisplay.classList.remove('capturing');
capturingDisplay.textContent =
capturingDisplay.getAttribute('data-prev') || '';
capturingAction = null;
capturingDisplay = null;
showStatus('ok', 'Cancelled');
return;
}
var bareMod = ['Control','Shift','Alt','Meta'].indexOf(e.key) >= 0;
if (bareMod) return;
var accel = buildAccel(e);
if (!accel) return;
capturingDisplay.classList.remove('capturing');
capturingDisplay.textContent = displayAccel(accel);
var action = capturingAction;
capturingAction = null;
capturingDisplay = null;
commitShortcut(action, accel);
}, true);
/* ── Toggle / save actions ──────────────────────────────────────── */
function toggle(feature) {
if (feature === 'theme_dark') {
var h = document.documentElement;
var isDark = h.classList.contains('dark');
if (isDark) h.classList.remove('dark');
else h.classList.add('dark');
var btn = document.getElementById('toggle_theme_dark');
if (btn) setToggleClass(btn, !isDark);
var x = new XMLHttpRequest();
x.open('GET', 'sovereign://settings/set?feature=' + feature, true);
x.send();
showStatus('ok', 'theme_dark: ' + (isDark ? 'OFF' : 'ON'));
return;
}
var x = new XMLHttpRequest();
x.open('GET', 'sovereign://settings/set?feature=' + feature, true);
x.onreadystatechange = function() {
if (x.readyState !== 4) return;
try {
var d = JSON.parse(x.responseText);
if (d.error) showStatus('err', d.message);
else {
showStatus('ok', d.key + ': ' + (d.value ? 'ON' : 'OFF'));
setTimeout(function() { location.reload(); }, 300);
}
} catch (e) { showStatus('err', 'Error: ' + e.message); }
};
x.send();
}
function save(key) {
var el = document.getElementById(key);
var val = el ? el.value : '';
var x = new XMLHttpRequest();
x.open('GET', 'sovereign://settings/set?key=' + encodeURIComponent(key) +
'&value=' + encodeURIComponent(val), true);
x.onreadystatechange = function() {
if (x.readyState !== 4) return;
try {
var d = JSON.parse(x.responseText);
if (d.error) showStatus('err', d.message);
else {
showStatus('ok', d.key + ' = ' + d.value);
if (d.reload) setTimeout(function() { location.reload(); }, 300);
}
} catch (e) { showStatus('err', 'Error: ' + e.message); }
};
x.send();
}
/* ── Init ───────────────────────────────────────────────────────── */
sovereignGet('sovereign://settings/config?_=' + Date.now())
.then(function(d) { applyConfig(d); })
.catch(function(e) {
showStatus('err', 'Failed to load settings: ' + e.message);
});