/* Settings navigation and extension registry. All controls stay mounted. */ (function (root) { 'use strict'; const clusters = ['Listening', 'Library & storage', 'Live & worship', 'Account & sync', 'Advanced & about']; const normalize = value => String(value || '').normalize('NFKD').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim(); function search(entries, query) { const words = normalize(query).split(' ').filter(Boolean); return entries.filter(entry => words.every(word => normalize(`${entry.title} ${entry.description || ''}`).includes(word))); } function createRegistry() { const entries = new Map(); return { register(section) { if (!section || !/^[a-z][a-z0-9-]*$/.test(section.id) || !section.title || typeof section.render !== 'function') throw new TypeError('A settings section needs an id, title and render function.'); entries.set(section.id, { cluster: 'Advanced & about', summary: () => '', ...section }); return () => { if (entries.get(section.id)?.render === section.render) entries.delete(section.id); }; }, list() { return [...entries.values()]; }, get(id) { return entries.get(id); }, }; } const registry = createRegistry(); let dispose = () => {}; const icon = paths => ``; function mount(host, context = {}) { dispose(); const document = host.ownerDocument, win = document.defaultView, sections = registry.list(), panels = new Map(), rows = new Map(), cleanups = []; let remembered = ''; try { remembered = win.sessionStorage.getItem('ytpSettingsSection') || ''; } catch {} let active = registry.get(remembered) ? remembered : sections[0]?.id; host.classList.add('settings-browser'); const toolbar = document.createElement('div'); toolbar.className = 'settings-search'; const input = document.createElement('input'); input.type = 'search'; input.placeholder = 'Search settings'; input.setAttribute('aria-label', 'Search settings and controls'); toolbar.append(input); const grid = document.createElement('div'); grid.className = 'settings-grid'; const navigation = document.createElement('nav'); navigation.className = 'settings-overview'; navigation.setAttribute('aria-label', 'Settings sections'); const detail = document.createElement('div'); detail.className = 'settings-detail'; const back = document.createElement('button'); back.type = 'button'; back.className = 'settings-back'; back.textContent = '← All settings'; detail.append(back); const results = document.createElement('div'); results.className = 'settings-results'; results.hidden = true; const empty = document.createElement('p'); empty.className = 'settings-empty'; empty.textContent = 'No settings match your search.'; empty.hidden = true; for (const cluster of [...clusters, ...new Set(sections.map(s => s.cluster).filter(c => !clusters.includes(c)))]) { const group = sections.filter(s => s.cluster === cluster); if (!group.length) continue; const heading = document.createElement('h3'); heading.textContent = cluster; navigation.append(heading); for (const section of group) { const button = document.createElement('button'); button.type = 'button'; button.className = 'settings-section-row'; button.dataset.section = section.id; button.innerHTML = icon(section.icon); const text = document.createElement('span'), title = document.createElement('b'), summary = document.createElement('small'); title.textContent = section.title; text.append(title, summary); button.append(text); button.setAttribute('aria-controls', 'settings-section-' + section.id); navigation.append(button); rows.set(section.id, { button, summary }); const panel = document.createElement('section'); panel.id = 'settings-section-' + section.id; panel.className = 'settings-section'; panel.setAttribute('aria-label', section.title); panels.set(section.id, panel); detail.append(panel); const cleanup = section.render(panel, context); if (typeof cleanup === 'function') cleanups.push(cleanup); button.addEventListener('click', () => open(section.id)); } } grid.append(navigation, detail); host.replaceChildren(toolbar, results, empty, grid); function summaries() { for (const section of sections) { try { rows.get(section.id).summary.textContent = section.summary(context) || ''; } catch {} } } function show(id, inDetail, focusId) { if (registry.get(id)) active = id; host.dataset.detail = inDetail ? '1' : '0'; for (const [key, panel] of panels) panel.hidden = key !== active; for (const [key, row] of rows) row.button.setAttribute('aria-current', key === active ? 'true' : 'false'); if (inDetail) { try { win.sessionStorage.setItem('ytpSettingsSection', active); } catch {} const target = focusId ? document.getElementById(focusId) : panels.get(active)?.querySelector('input,select,button,textarea,summary'); if (target) { const fold = target.closest('details'); if (fold) fold.open = true; target.focus({ preventScroll: true }); target.scrollIntoView({ block: 'center', behavior: 'instant' }); } } } function open(id, focusId) { if (!panels.has(id)) return; input.value = ''; filter(); win.history.pushState({ ...win.history.state, ytpSettingsSection: id }, '', win.location.href); show(id, true, focusId); } function filter() { const query = input.value.trim(); results.replaceChildren(); results.hidden = !query; grid.hidden = !!query; if (!query) { empty.hidden = true; return; } const entries = []; for (const section of sections) { entries.push({ title: section.title, description: section.description || '', section: section.id }); for (const el of panels.get(section.id).querySelectorAll('.set-row, .set-actions button, [data-settings-search]')) { const control = el.matches('button,input,select') ? el : el.querySelector('input,select,button,textarea,summary'); if (control && !control.id) control.id = `settings-control-${section.id}-${entries.length}`; const title = (el.querySelector('span')?.childNodes[0]?.textContent || el.textContent).trim(); entries.push({ title, description: `${section.title} ${el.textContent}`, section: section.id, control: control?.id }); } } const matches = search(entries, query); empty.hidden = matches.length > 0; for (const item of matches.slice(0, 40)) { const button = document.createElement('button'); button.type = 'button'; button.className = 'settings-section-row'; const text = document.createElement('span'); text.textContent = item.title; const subtitle = document.createElement('small'); subtitle.textContent = registry.get(item.section).title; text.append(subtitle); button.append(text); button.addEventListener('click', () => open(item.section, item.control)); results.append(button); } } back.addEventListener('click', () => { if (win.history.state?.ytpSettingsSection) win.history.back(); else show(active, false); }); const pop = event => { if (!host.isConnected) return; show(event.state?.ytpSettingsSection || active, !!event.state?.ytpSettingsSection); if (!event.state?.ytpSettingsSection) rows.get(active)?.button.focus({ preventScroll: true }); }; win.addEventListener('popstate', pop); input.addEventListener('input', filter); host.addEventListener('change', summaries); host.addEventListener('input', summaries); const observer = new win.MutationObserver(summaries); observer.observe(detail, { childList: true, subtree: true, characterData: true }); const state = win.history.state?.ytpSettingsSection; if (!state) win.history.replaceState({ ...win.history.state, ytpSettingsSection: null }, '', win.location.href); show(state || active, !!state); summaries(); dispose = () => { observer.disconnect(); win.removeEventListener('popstate', pop); cleanups.forEach(fn => fn()); }; return { open, refresh: summaries, dispose }; } const api = { ...registry, mount, search, normalize, createRegistry, clusters }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.SettingsSections = api; })(typeof globalThis !== 'undefined' ? globalThis : this);