mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-19 13:22:20 +02:00
refactor(settings): add registry-backed navigation and finder (#6040)
* refactor(settings): add modular shell primitives * refactor(settings): wire modular shell * test(settings): exercise real coordinator ESM boundary * refactor(settings): add registry-backed settings finder * fix(settings): harden registry navigation behavior
This commit is contained in:
+83
-150
@@ -3,8 +3,25 @@
|
||||
|
||||
import uiModule from './ui.js';
|
||||
import searchModule from './search.js';
|
||||
import { makeWindowDraggable } from './windowDrag.js';
|
||||
import { clearDockSide } from './modalSnap.js';
|
||||
import { byId } from './settings/dom.js';
|
||||
import {
|
||||
getSettingsRegistryIssues,
|
||||
isAdminManagedSettingsTab,
|
||||
} from './settings/registry.js';
|
||||
import { bindSettingsSearch } from './settings/search.js';
|
||||
import { bindSettingsSidebar } from './settings/sidebar.js';
|
||||
import {
|
||||
activateSettingsPanel,
|
||||
getActiveSettingsTab,
|
||||
bindSettingsNavigation,
|
||||
} from './settings/navigation.js';
|
||||
import {
|
||||
bindSettingsDrag,
|
||||
bindSettingsClose,
|
||||
bindOpenPromptModalLink,
|
||||
showSettingsModal,
|
||||
hideSettingsModal,
|
||||
} from './settings/lifecycle.js';
|
||||
import { sortModelIds } from './modelSort.js';
|
||||
import { providerLogo } from './providers.js';
|
||||
import { isAltGrEvent } from './platform.js';
|
||||
@@ -14,133 +31,29 @@ let initialized = false;
|
||||
let modalEl = null;
|
||||
let _authPolicy = { password_min_length: 8 };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
const el = byId;
|
||||
function esc(s) { return uiModule.esc(s); }
|
||||
function safeRasterDataUrl(raw) {
|
||||
const value = String(raw || '').trim();
|
||||
return /^data:image\/(?:png|jpe?g|gif|webp);base64,[a-z0-9+/=\s]+$/i.test(value) ? value : '';
|
||||
}
|
||||
|
||||
/* ── Tab switching ── */
|
||||
const ADMIN_TABS = new Set(['services', 'added-models', 'integrations', 'tools', 'users', 'system']);
|
||||
/* ── Settings shell coordination ── */
|
||||
function onSettingsPanelActivated(tab) {
|
||||
// Appearance keeps its existing transparent preview behavior.
|
||||
document.body.classList.toggle('settings-appearance-open', tab === 'appearance');
|
||||
syncAppearanceOpacity(tab === 'appearance');
|
||||
|
||||
function initTabs() {
|
||||
modalEl.querySelectorAll('[data-settings-tab]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tab = btn.dataset.settingsTab;
|
||||
// Lazy-init admin when first clicking an admin tab
|
||||
if (ADMIN_TABS.has(tab) && window.adminModule && typeof window.adminModule.open === 'function') {
|
||||
window.adminModule.open(tab);
|
||||
return;
|
||||
}
|
||||
modalEl.querySelectorAll('[data-settings-tab]').forEach(b => b.classList.toggle('active', b.dataset.settingsTab === tab));
|
||||
modalEl.querySelectorAll('[data-settings-panel]').forEach(p => p.classList.toggle('hidden', p.dataset.settingsPanel !== tab));
|
||||
// Mark when the Appearance tab is open so the modal can go
|
||||
// semi-transparent — lets the user see the rest of the UI react as
|
||||
// they flip toggles instead of having to close + reopen the modal.
|
||||
document.body.classList.toggle('settings-appearance-open', tab === 'appearance');
|
||||
syncAppearanceOpacity(tab === 'appearance');
|
||||
if (tab === 'ai') refreshAiModelEndpoints();
|
||||
});
|
||||
});
|
||||
// AI endpoints are intentionally refreshed only when entering the AI panel.
|
||||
if (tab === 'ai') refreshAiModelEndpoints();
|
||||
}
|
||||
|
||||
/* ── Dragging ── */
|
||||
function initDrag() {
|
||||
const header = modalEl.querySelector('.modal-header');
|
||||
const content = modalEl.querySelector('.settings-modal-content');
|
||||
if (!header || !content) return;
|
||||
// Skip interactive controls in the header (e.g. the opacity slider) so
|
||||
// grabbing them doesn't start a window-drag.
|
||||
makeWindowDraggable(modalEl, {
|
||||
content,
|
||||
header,
|
||||
skipSelector: 'button, input, select, .theme-opacity-wrap',
|
||||
enableDock: true,
|
||||
});
|
||||
}
|
||||
|
||||
function resetWindowPlacement() {
|
||||
const content = modalEl && modalEl.querySelector('.settings-modal-content');
|
||||
if (!content) return;
|
||||
const hadLeft = modalEl.classList.contains('modal-left-docked');
|
||||
const hadRight = modalEl.classList.contains('modal-right-docked');
|
||||
modalEl.classList.remove('modal-left-docked', 'modal-right-docked');
|
||||
if (hadLeft) clearDockSide('left', modalEl);
|
||||
if (hadRight) clearDockSide('right', modalEl);
|
||||
if (content._leftDockNavObs) {
|
||||
try { content._leftDockNavObs.navObs && content._leftDockNavObs.navObs.disconnect(); } catch (_) {}
|
||||
try { window.removeEventListener('resize', content._leftDockNavObs.reanchor); } catch (_) {}
|
||||
delete content._leftDockNavObs;
|
||||
function openAdminSettingsTab(tab) {
|
||||
if (window.adminModule && typeof window.adminModule.open === 'function') {
|
||||
window.adminModule.open(tab);
|
||||
return true;
|
||||
}
|
||||
delete content._preDockSnapshot;
|
||||
delete content._dockSide;
|
||||
delete content._dockSuspended;
|
||||
delete content.dataset._tilePreSnap;
|
||||
delete content.dataset._tileZone;
|
||||
[
|
||||
'position', 'left', 'top', 'right', 'bottom', 'margin', 'transform',
|
||||
'width', 'height', 'max-width', 'max-height', 'border-radius', 'transition',
|
||||
].forEach(prop => content.style.removeProperty(prop));
|
||||
}
|
||||
|
||||
/* ── Delegated link: close Settings + open the Prompt (characters) modal ── */
|
||||
function initOpenPromptModalLink() {
|
||||
document.addEventListener('click', async (e) => {
|
||||
const link = e.target.closest('[data-open-prompt-modal]');
|
||||
if (!link) return;
|
||||
e.preventDefault();
|
||||
// Close settings first so the prompt modal isn't stacked on top.
|
||||
if (modalEl && !modalEl.classList.contains('hidden')) close();
|
||||
try {
|
||||
const m = await import('./presets.js');
|
||||
const fn = m.openCustomPresetModal || (m.default && m.default.openCustomPresetModal);
|
||||
if (typeof fn === 'function') fn();
|
||||
} catch (_) {
|
||||
const modal = document.getElementById('custom-preset-modal');
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
}
|
||||
// Force the Persona tab (data-chartab="character") since the link's
|
||||
// whole purpose is editing personas — not landing on Inject by default.
|
||||
const personaTab = document.querySelector('#custom-preset-modal .preset-tab[data-chartab="character"]');
|
||||
if (personaTab) personaTab.click();
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Close on backdrop / X ── */
|
||||
function initClose() {
|
||||
modalEl.querySelector('.close-btn').addEventListener('click', close);
|
||||
modalEl.addEventListener('mousedown', e => {
|
||||
if (uiModule.isTouchInsideModal()) return;
|
||||
if (e.target === modalEl) close();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape' || !modalEl || modalEl.classList.contains('hidden')) return;
|
||||
// Bail when a transient popover inside the modal is open — Esc should
|
||||
// dismiss just that, not the whole modal. Same-document listeners fire
|
||||
// in registration order regardless of capture/bubble, so the popover's
|
||||
// own handler can't pre-empt ours; we have to opt out here.
|
||||
const popoverOpen = modalEl.querySelector(
|
||||
'#adm-epLocalMoreMenu, #adm-epApiMoreMenu, #adm-provider-menu, #search-provider-menu, [data-popover-open="1"]'
|
||||
);
|
||||
if (popoverOpen && popoverOpen.style.display !== 'none' && !popoverOpen.classList.contains('hidden')) {
|
||||
return;
|
||||
}
|
||||
// If an integration edit/add form is open inside the modal, close
|
||||
// just that — don't dismiss the whole settings modal. (Pressing
|
||||
// ESC mid-edit and losing the modal was a fast-typing footgun.)
|
||||
const innerForm = modalEl.querySelector('#unified-intg-form, #set-email-accounts-form');
|
||||
if (innerForm && innerForm.style.display !== 'none' && innerForm.children.length > 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
innerForm.style.display = 'none';
|
||||
innerForm.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ── Appearance-tab opacity slider ──
|
||||
@@ -2238,10 +2151,39 @@ function initAccount() {
|
||||
|
||||
function initAll() {
|
||||
modalEl = el('settings-modal');
|
||||
initTabs();
|
||||
initDrag();
|
||||
initClose();
|
||||
initOpenPromptModalLink();
|
||||
|
||||
bindSettingsNavigation(modalEl, {
|
||||
openAdminTab: openAdminSettingsTab,
|
||||
onPanelActivated: onSettingsPanelActivated,
|
||||
});
|
||||
|
||||
bindSettingsSearch(modalEl, {
|
||||
isAdmin: () => !!window._isAdmin,
|
||||
openPanel(tab) {
|
||||
const button = modalEl.querySelector(`[data-settings-tab="${tab}"]`);
|
||||
if (button) button.click();
|
||||
},
|
||||
});
|
||||
|
||||
bindSettingsSidebar(modalEl);
|
||||
|
||||
const registryIssues = getSettingsRegistryIssues(modalEl);
|
||||
if (registryIssues.length) {
|
||||
console.warn('Settings registry/DOM mismatch:', registryIssues);
|
||||
}
|
||||
|
||||
bindSettingsDrag(modalEl);
|
||||
|
||||
bindSettingsClose(modalEl, {
|
||||
closeSettings: close,
|
||||
isTouchInsideModal: () => uiModule.isTouchInsideModal(),
|
||||
});
|
||||
|
||||
bindOpenPromptModalLink({
|
||||
getModal: () => modalEl,
|
||||
closeSettings: close,
|
||||
});
|
||||
|
||||
initOpacityToggle();
|
||||
initialized = true;
|
||||
initDefaultChat();
|
||||
@@ -5661,44 +5603,35 @@ function syncAdminVisibility() {
|
||||
═══════════════════════════════════════════ */
|
||||
export function open(tab) {
|
||||
if (!initialized) initAll();
|
||||
|
||||
syncAppearanceCheckboxes();
|
||||
if (modalEl.classList.contains('hidden')) {
|
||||
resetWindowPlacement();
|
||||
}
|
||||
modalEl.classList.remove('hidden');
|
||||
showSettingsModal(modalEl);
|
||||
syncAdminVisibility();
|
||||
const content = modalEl.querySelector('.settings-modal-content');
|
||||
|
||||
if (tab) {
|
||||
modalEl.querySelectorAll('[data-settings-tab]').forEach(b => b.classList.toggle('active', b.dataset.settingsTab === tab));
|
||||
modalEl.querySelectorAll('[data-settings-panel]').forEach(p => p.classList.toggle('hidden', p.dataset.settingsPanel !== tab));
|
||||
activateSettingsPanel(modalEl, tab);
|
||||
}
|
||||
// Auto-init admin data if showing an admin tab
|
||||
const activeTab = tab || (modalEl.querySelector('[data-settings-tab].active') || {}).dataset?.settingsTab || 'services';
|
||||
document.body.classList.toggle('settings-appearance-open', activeTab === 'appearance');
|
||||
syncAppearanceOpacity(activeTab === 'appearance');
|
||||
if (activeTab === 'ai') refreshAiModelEndpoints();
|
||||
if (ADMIN_TABS.has(activeTab) && window.adminModule && !window.adminModule._initialized) {
|
||||
|
||||
// Preserve existing panel-specific side effects when Settings is opened
|
||||
// directly to a tab as well as when the user navigates there.
|
||||
const activeTab = tab || getActiveSettingsTab(modalEl);
|
||||
onSettingsPanelActivated(activeTab);
|
||||
|
||||
// Auto-init admin data if showing an admin tab.
|
||||
if (isAdminManagedSettingsTab(activeTab) && window.adminModule && !window.adminModule._initialized) {
|
||||
window.adminModule._initData();
|
||||
}
|
||||
}
|
||||
|
||||
export function close() {
|
||||
if (!modalEl) return;
|
||||
// Always clear the appearance-tab body class so the rest of the app
|
||||
// doesn't keep its dimmed state if the modal got closed mid-tab.
|
||||
|
||||
// Always clear the Appearance state so the rest of the app does not remain
|
||||
// dimmed if Settings is closed while that panel is active.
|
||||
document.body.classList.remove('settings-appearance-open');
|
||||
syncAppearanceOpacity(false); // clear any opacity-slider fade
|
||||
const content = modalEl.querySelector('.modal-content, .settings-modal-content');
|
||||
if (content && !content.classList.contains('modal-closing')) {
|
||||
content.classList.add('modal-closing');
|
||||
content.addEventListener('animationend', () => {
|
||||
modalEl.classList.add('hidden');
|
||||
content.classList.remove('modal-closing');
|
||||
}, { once: true });
|
||||
setTimeout(() => { if (!modalEl.classList.contains('hidden')) { modalEl.classList.add('hidden'); content.classList.remove('modal-closing'); } }, 250);
|
||||
} else {
|
||||
modalEl.classList.add('hidden');
|
||||
}
|
||||
syncAppearanceOpacity(false);
|
||||
|
||||
hideSettingsModal(modalEl);
|
||||
}
|
||||
|
||||
// Handle redirect back from Google OAuth2 — open settings to integrations and show status.
|
||||
|
||||
Reference in New Issue
Block a user