// SettingsModal.tsx — 居中弹窗,左侧单层侧栏。 // // 重构(2026-05):原本是「外层弹窗侧栏 + 设置页内层侧栏」双层嵌套,用户点 // 「设置」还要再面对第二个侧栏。现在拍平成单层 —— 通用 / 服务 / 隐私 / 高级 / // 个性化 / 关于 六个 tab + 帮助外链组。每个 tab 的内容见 pages/settings/。 // // 设计原则:每个可见控件都必须可用。没有后端支撑的占位(账号 / 主题切换 等) // 不在此弹窗出现。 import { useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { SavedToast } from './SavedToast'; import { useSavedToastListener } from '../lib/savedEvent'; import { openExternal } from '../lib/ipc'; import type { OS } from './WindowChrome'; import { GeneralTab, ServicesTab, PrivacyTab, AdvancedTab } from '../pages/settings/tabs'; import { AboutSection } from '../pages/settings/AboutSection'; // 稳定 tab ID(与 i18n key `modal.sections.*` 一致)。 export type SettingsSectionId = | 'general' | 'services' | 'privacy' | 'advanced' | 'about'; interface SettingsModalProps { os: OS; onClose: () => void; initialSettingsSection?: SettingsSectionId; } interface ModalNavItem { id: string; icon: string; external?: boolean; href?: string; } const HELP_URL = 'https://github.com/appergb/openless#readme'; const RELEASE_NOTES_URL = 'https://github.com/appergb/openless/releases'; // 第一组:可选中的 tab;第二组:外部链接(永远不 active)。 const TAB_ITEMS: ModalNavItem[] = [ { id: 'general', icon: 'settings' }, { id: 'services', icon: 'cloud' }, { id: 'privacy', icon: 'shield' }, { id: 'advanced', icon: 'bolt' }, { id: 'about', icon: 'info' }, ]; const LINK_ITEMS: ModalNavItem[] = [ { id: 'helpCenter', icon: 'help', external: true, href: HELP_URL }, { id: 'releaseNotes', icon: 'doc', external: true, href: RELEASE_NOTES_URL }, ]; export function SettingsModal({ os: _os, onClose, initialSettingsSection }: SettingsModalProps) { const { t } = useTranslation(); const [section, setSection] = useState(initialSettingsSection ?? 'general'); const savedToast = useSavedToastListener(); // 与 sidebar nav 一致的滑动指示器:仅 tab 组有 pill;外链组永远不画 pill。 const tabRefs = useRef>([]); const [pillRect, setPillRect] = useState<{ top: number; height: number } | null>(null); useLayoutEffect(() => { const idx = TAB_ITEMS.findIndex(it => it.id === section); const el = tabRefs.current[idx]; if (!el) return; setPillRect({ top: el.offsetTop, height: el.offsetHeight }); }, [section]); return (
e.stopPropagation()} style={{ width: '100%', maxWidth: 880, height: '100%', maxHeight: 600, background: 'var(--ol-surface)', borderRadius: 14, border: '0.5px solid rgba(0,0,0,.08)', boxShadow: '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)', display: 'flex', overflow: 'hidden', animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)', position: 'relative', }}> {/* ─── 单层侧栏 ────────────────────────────────────────────── */} {/* ─── 内容区 ────────────────────────────────────────────── 父容器 overflow:hidden + 列向 flex;关闭按钮、section 标题固定在头部, 只有最里层的 scroll wrapper 真正滚动。 */}
{/* "已保存" toast:right:54 避开 28×28 关闭按钮 + 12px gap。 */}

{t(`modal.sections.${section}`)}

{/* key=section 让切 tab 时整块重挂载,ol-tab-fade 轻微淡入。 */}
{section === 'general' && } {section === 'services' && } {section === 'privacy' && } {section === 'advanced' && } {section === 'about' && }
); } const navBtnStyle = { display: 'flex', alignItems: 'center', gap: 10, padding: '7px 10px', borderRadius: 8, border: 0, background: 'transparent', fontFamily: 'inherit', fontSize: 13, cursor: 'default', textAlign: 'left' as const, position: 'relative' as const, zIndex: 1, transition: 'color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick)', };