feat: reorganize settings dialog — add Multi-Device tab, enable/disable toggle, device name as page title

- Add multi_device_enabled: false to DEFAULT_SETTINGS in settings.py
- Add fifth 'Multi-Device' tab (data-tab='devices') to settings dialog sidebar
- Move #setting-device-name, #setting-remote-instances, #add-remote-instance-btn,
  #setting-view-mode, and #setting-view-scope out of Display/Sessions tabs into
  the new dedicated devices panel
- Add #setting-multi-device-enabled checkbox at top of Multi-Device tab;
  when unchecked, all other fields in the tab are greyed out and disabled
- Gate buildSources() remote instances behind multi_device_enabled (smart default:
  treated as enabled if remote_instances is non-empty for backward compat)
- Wire device name → document.title: updates title on openSettings() and on
  every keystroke in the device name input field
- Add _updateMultiDeviceFieldsState() helper to enable/disable fields
- Add #multi-device-fields CSS transition for smooth opacity animation
- Add 19 new tests (settings, HTML, JS, CSS) covering all new behavior
This commit is contained in:
Brian Krabach
2026-03-31 06:16:44 -07:00
parent 881b6cc9f3
commit 679416a5bf
8 changed files with 380 additions and 58 deletions
+60 -11
View File
@@ -1380,17 +1380,23 @@ function buildSources(settings) {
var sources = [
{ url: '', name: localName, type: 'local', status: 'authenticated', backoffMs: 2000 },
];
// Only add remote sources when multi-device is enabled.
// Smart default: treat as enabled if remote_instances is non-empty (backward compat).
var remotes = (settings && settings.remote_instances) || [];
for (var i = 0; i < remotes.length; i++) {
var r = remotes[i];
if (r && r.url) {
sources.push({
url: r.url.replace(/\/+$/, ''),
name: r.name || r.url,
type: 'remote',
status: 'authenticated',
backoffMs: 2000,
});
var multiDeviceEnabled = (settings && settings.multi_device_enabled) ||
(remotes.length > 0);
if (multiDeviceEnabled) {
for (var i = 0; i < remotes.length; i++) {
var r = remotes[i];
if (r && r.url) {
sources.push({
url: r.url.replace(/\/+$/, ''),
name: r.name || r.url,
type: 'remote',
status: 'authenticated',
backoffMs: 2000,
});
}
}
}
return sources;
@@ -1449,6 +1455,25 @@ function _saveRemoteInstances() {
});
}
// ─── Multi-Device helper ──────────────────────────────────────────────────────────
/**
* Enable or disable all Multi-Device tab fields (except the enable toggle itself).
* When disabled, the fields container gets opacity: 0.5 and inputs/selects/buttons
* are disabled so users cannot interact with them.
* @param {boolean} enabled
*/
function _updateMultiDeviceFieldsState(enabled) {
var fieldsContainer = $('multi-device-fields');
if (!fieldsContainer) return;
var controls = fieldsContainer.querySelectorAll('input, select, button');
controls.forEach(function(ctrl) {
ctrl.disabled = !enabled;
});
fieldsContainer.style.opacity = enabled ? '' : '0.5';
}
// ─── Settings dialog ──────────────────────────────────────────────────────────
/**
@@ -1766,6 +1791,20 @@ function openSettings() {
deviceNameEl.value = (ss && ss.device_name) || '';
}
// Update document.title from device_name setting
if (ss && ss.device_name) {
document.title = ss.device_name;
}
// Multi-device enabled checkbox (with smart default: checked if remote_instances non-empty)
const multiDeviceEnabledEl = $('setting-multi-device-enabled');
if (multiDeviceEnabledEl) {
var remoteList = (ss && ss.remote_instances) || [];
multiDeviceEnabledEl.checked = !!(ss && ss.multi_device_enabled) ||
remoteList.length > 0;
_updateMultiDeviceFieldsState(multiDeviceEnabledEl.checked);
}
// Remote instances
const remoteInstancesEl = $('setting-remote-instances');
if (remoteInstancesEl) {
@@ -2298,11 +2337,21 @@ function bindStaticEventListeners() {
patchServerSetting('new_session_template', NEW_SESSION_DEFAULT_TEMPLATE);
});
// Sessions tab — device name with 500ms debounce
// Multi-Device tab — enable/disable toggle
on($('setting-multi-device-enabled'), 'change', function() {
var enabled = this.checked;
_updateMultiDeviceFieldsState(enabled);
patchServerSetting('multi_device_enabled', enabled).then(function() {
_sources = buildSources(_serverSettings);
});
});
// Multi-Device tab — device name with 500ms debounce; updates document.title immediately
var _deviceNameDebounceTimer;
on($('setting-device-name'), 'input', function() {
clearTimeout(_deviceNameDebounceTimer);
var val = this.value;
if (val) document.title = val;
_deviceNameDebounceTimer = setTimeout(function() {
patchServerSetting('device_name', val).then(function() {
_sources = buildSources(_serverSettings);
+33 -24
View File
@@ -90,6 +90,7 @@
<button class="settings-tab" data-tab="sessions">Sessions</button>
<button class="settings-tab" data-tab="notifications">Notifications</button>
<button class="settings-tab" data-tab="new-session">Commands</button>
<button class="settings-tab" data-tab="devices">Multi-Device</button>
</nav>
<div class="settings-content">
<div class="settings-panel" data-tab="display">
@@ -123,21 +124,6 @@
<option value="4">4</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-view-mode">View Mode</label>
<select id="setting-view-mode" class="settings-select">
<option value="flat">Flat</option>
<option value="grouped">Grouped</option>
<option value="filtered">Filtered</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-view-scope">View Scope</label>
<select id="setting-view-scope" class="settings-select">
<option value="local">Local</option>
<option value="server">Server</option>
</select>
</div>
</div>
<div class="settings-panel hidden" data-tab="sessions">
<div class="settings-field">
@@ -166,15 +152,6 @@
<label class="settings-label" for="setting-auto-open">Auto-open created sessions</label>
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-device-name">Device Name</label>
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
</div>
<div class="settings-field settings-field--column">
<label class="settings-label">Remote Instances</label>
<div id="setting-remote-instances" class="settings-remote-list"></div>
<button id="add-remote-instance-btn" class="settings-action-btn">+ Add Remote</button>
</div>
</div>
<div class="settings-panel hidden" data-tab="notifications">
<div class="settings-field">
@@ -203,6 +180,38 @@
<button id="setting-delete-template-reset" class="settings-action-btn">Reset to default</button>
</div>
</div>
<div class="settings-panel hidden" data-tab="devices">
<div class="settings-field">
<label class="settings-label" for="setting-multi-device-enabled">Enable multi-device support</label>
<input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" />
</div>
<div id="multi-device-fields">
<div class="settings-field">
<label class="settings-label" for="setting-device-name">Device Name</label>
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-view-mode">View Mode</label>
<select id="setting-view-mode" class="settings-select">
<option value="flat">Flat</option>
<option value="grouped">Grouped</option>
<option value="filtered">Filtered</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-view-scope">View Scope</label>
<select id="setting-view-scope" class="settings-select">
<option value="local">Local</option>
<option value="server">Server</option>
</select>
</div>
<div class="settings-field settings-field--column">
<label class="settings-label">Remote Instances</label>
<div id="setting-remote-instances" class="settings-remote-list"></div>
<button id="add-remote-instance-btn" class="settings-action-btn">+ Add Remote</button>
</div>
</div>
</div>
</div>
</div>
</dialog>
+5
View File
@@ -1653,3 +1653,8 @@ body {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
/* Multi-Device tab — smooth enable/disable opacity transition */
#multi-device-fields {
transition: opacity 0.2s ease;
}