import { useState, useEffect, type ReactNode } from 'react'; import type { AppSettings, AppSettingsUpdate, Channel, Contact, HealthStatus, RadioAdvertMode, RadioConfig, RadioConfigUpdate, RadioDiscoveryResponse, RadioDiscoveryTarget, } from '../types'; import type { LocalLabel } from '../utils/localLabel'; import { SETTINGS_SECTION_ICONS, SETTINGS_SECTION_LABELS, type SettingsSection, } from './settings/settingsConstants'; import { SettingsRadioSection } from './settings/SettingsRadioSection'; import { SettingsLocalSection } from './settings/SettingsLocalSection'; import { SettingsFanoutSection } from './settings/SettingsFanoutSection'; import { SettingsDatabaseSection } from './settings/SettingsDatabaseSection'; import { SettingsStatisticsSection } from './settings/SettingsStatisticsSection'; import { SettingsAboutSection } from './settings/SettingsAboutSection'; interface SettingsModalBaseProps { open: boolean; pageMode?: boolean; config: RadioConfig | null; health: HealthStatus | null; appSettings: AppSettings | null; onClose: () => void; onSave: (update: RadioConfigUpdate) => Promise; onSaveAppSettings: (update: AppSettingsUpdate) => Promise; onSetPrivateKey: (key: string) => Promise; onReboot: () => Promise; onDisconnect: () => Promise; onReconnect: () => Promise; onAdvertise: (mode: RadioAdvertMode) => Promise; meshDiscovery: RadioDiscoveryResponse | null; meshDiscoveryLoadingTarget: RadioDiscoveryTarget | null; onDiscoverMesh: (target: RadioDiscoveryTarget) => Promise; onHealthRefresh: () => Promise; onRefreshAppSettings: () => Promise; onLocalLabelChange?: (label: LocalLabel) => void; blockedKeys?: string[]; blockedNames?: string[]; onToggleBlockedKey?: (key: string) => void; onToggleBlockedName?: (name: string) => void; contacts?: Contact[]; channels?: Channel[]; onBulkDeleteContacts?: (deletedKeys: string[]) => void; trackedTelemetryRepeaters?: string[]; onToggleTrackedTelemetry?: (publicKey: string) => Promise; trackedTelemetryContacts?: string[]; onToggleTrackedTelemetryContact?: (publicKey: string) => Promise; } export type SettingsModalProps = SettingsModalBaseProps & ( | { externalSidebarNav: true; desktopSection: SettingsSection } | { externalSidebarNav?: false; desktopSection?: never } ); export function SettingsModal(props: SettingsModalProps) { const { open, pageMode = false, config, health, appSettings, onClose, onSave, onSaveAppSettings, onSetPrivateKey, onReboot, onDisconnect, onReconnect, onAdvertise, meshDiscovery, meshDiscoveryLoadingTarget, onDiscoverMesh, onHealthRefresh, onRefreshAppSettings, onLocalLabelChange, blockedKeys, blockedNames, onToggleBlockedKey, onToggleBlockedName, contacts, channels, onBulkDeleteContacts, trackedTelemetryRepeaters, onToggleTrackedTelemetry, trackedTelemetryContacts, onToggleTrackedTelemetryContact, } = props; const externalSidebarNav = props.externalSidebarNav === true; const desktopSection = props.externalSidebarNav ? props.desktopSection : undefined; const getIsMobileLayout = () => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false; return window.matchMedia('(max-width: 767px)').matches; }; const [isMobileLayout, setIsMobileLayout] = useState(getIsMobileLayout); const externalDesktopSidebarMode = externalSidebarNav && !isMobileLayout; const [expandedSections, setExpandedSections] = useState>({ radio: false, local: false, fanout: false, database: false, statistics: false, about: false, }); // Refresh settings from server when modal opens useEffect(() => { if (open || pageMode) { onRefreshAppSettings(); } }, [open, pageMode, onRefreshAppSettings]); useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; const query = window.matchMedia('(max-width: 767px)'); const onChange = (event: MediaQueryListEvent) => { setIsMobileLayout(event.matches); }; setIsMobileLayout(query.matches); if (typeof query.addEventListener === 'function') { query.addEventListener('change', onChange); return () => query.removeEventListener('change', onChange); } query.addListener(onChange); return () => query.removeListener(onChange); }, []); const toggleSection = (section: SettingsSection) => { setExpandedSections((prev) => ({ ...prev, [section]: !prev[section], })); }; const isSectionVisible = (section: SettingsSection) => externalDesktopSidebarMode ? desktopSection === section : expandedSections[section]; const showSectionButton = !externalDesktopSidebarMode; const shouldRenderSection = (section: SettingsSection) => !externalDesktopSidebarMode || desktopSection === section; const sectionWrapperClass = ''; const sectionContentClass = externalDesktopSidebarMode ? 'mx-auto w-full max-w-[800px] space-y-4 p-4' : 'mx-auto w-full max-w-[800px] space-y-4 border-t border-input p-4'; const settingsContainerClass = externalDesktopSidebarMode ? 'w-full h-full min-w-0 overflow-x-hidden overflow-y-auto [contain:layout_paint]' : 'w-full h-full min-w-0 overflow-x-hidden overflow-y-auto space-y-3 [contain:layout_paint]'; const sectionButtonClasses = 'w-full flex items-center justify-between px-4 py-3 text-left hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset'; const renderSectionHeader = (section: SettingsSection): ReactNode => { if (!showSectionButton) return null; const Icon = SETTINGS_SECTION_ICONS[section]; return ( ); }; if (!pageMode && !open) { return null; } return (
{shouldRenderSection('radio') && (
{renderSectionHeader('radio')} {isSectionVisible('radio') && (config && appSettings ? ( ) : (
Radio is not available.
))}
)} {shouldRenderSection('local') && (
{renderSectionHeader('local')} {isSectionVisible('local') && ( )}
)} {shouldRenderSection('database') && (
{renderSectionHeader('database')} {isSectionVisible('database') && (appSettings ? ( ) : (
Loading app settings...
))}
)} {shouldRenderSection('fanout') && (
{renderSectionHeader('fanout')} {isSectionVisible('fanout') && ( )}
)} {shouldRenderSection('statistics') && (
{renderSectionHeader('statistics')} {isSectionVisible('statistics') && ( )}
)} {shouldRenderSection('about') && (
{renderSectionHeader('about')} {isSectionVisible('about') && ( )}
)}
); }