327 lines
11 KiB
JavaScript
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);
|
|
});
|