import React, { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'; import { useParams } from 'react-router-dom'; import { SETTINGS_PAGE_OVERRIDES } from '../data/settingsOverrides'; import { SettingsHeader } from './SettingsHeader'; import { PreferenceCategory } from './PreferenceCategory'; import { PreferenceItem } from './PreferenceItem'; import { SwitchPreference } from './SwitchPreference'; import { SeekBarPreference } from './SeekBarPreference'; import { ListPreference } from './ListPreference'; import { ValuePreference } from './ValuePreference'; import { Toast } from '@/os/components/Toast'; import { WifiSavedNetworksPage } from './WifiSavedNetworksPage'; import { WifiSavedNetworkDetailPage } from './WifiSavedNetworkDetailPage'; import { WifiNetworksPage } from './WifiNetworksPage'; import { BluetoothDevicesPage } from './BluetoothDevicesPage'; import { StorageDashboardPage } from './StorageDashboardPage'; import { NotificationManagingPage } from './NotificationManagingPage'; import { NotificationAppDetailPage } from './NotificationAppDetailPage'; import { AppPermissionsPage } from './AppPermissionsPage'; import { AppPermissionDetailPage } from './AppPermissionDetailPage'; import { LauncherSettingsPage } from './LauncherSettingsPage'; import { SilentModeSettingsPage } from './SilentModeSettingsPage'; import { LanguagePickerPage } from './LanguagePickerPage'; import type { SettingsItem, SettingsMainSection, SettingsPage } from '../types'; import { ClipboardService } from '../../../os/ClipboardService'; import { routeGetPreference } from '../../../os/managers/registry'; import { getOsDataRevision, subscribeOsDataRevision } from '../../../os/simState'; import { useSettingsStrings } from '../res/useSettingsStrings'; import { useSettingsStore, selectPagesData, selectPagesLoading, selectPagesError } from '../state'; import { useSettingsGestures } from '../hooks/useSettingsGestures'; const PAGE_ID_ALIASES: Record = { // System: this page is built dynamically; we reuse a close static screen. security_settings: { targetId: 'security_settings_common' }, }; const INFO_VALUE_PAGES = new Set([ 'my_device_settings', 'my_device_detail_settings', 'my_device_info_pref_screen', 'hardware_info', 'battery_info', // SIM / radio status pages 'device_info_sim_status', 'status_sim', ]); // Theme / wallpaper / icons entries should open Theme Store app directly. const THEME_STORE_KEYS = new Set([ // personalize_title 'theme_center', 'wallpaper_center', 'font_center', // theme_settings 'theme_store', 'my_themes', 'icon_pack', 'font_style', 'system_sounds', 'notification_shade', // wallpaper_settings 'lock_wallpaper', 'home_wallpaper', 'super_wallpaper', 'live_wallpaper', 'dynamic_wallpaper', 'effect_wallpaper', 'wallpaper_carousel', ]); function formatValue(v: any, s: { on: string; off: string }): string | undefined { if (v === undefined || v === null) return undefined; if (typeof v === 'boolean') return v ? s.on : s.off; if (typeof v === 'number') return String(v); if (typeof v === 'string') return v; return String(v); } /** Lookup a page by id: hand-written overrides first, then auto-generated */ function findPage(pageId: string, pages: Record | null) { const alias = PAGE_ID_ALIASES[pageId]; const effectiveId = alias?.targetId || pageId; // Prefer hand-written overrides when present (to emulate programmatic pages) const base = SETTINGS_PAGE_OVERRIDES[effectiveId] || pages?.[effectiveId] || undefined; if (!base) return undefined; if (alias) { // Keep the requested id for routing, but reuse the aliased categories. return { ...base, id: pageId, title: alias.title || base.title }; } return base; } function buildMainTargetPageTitleMap(sections: SettingsMainSection[] | null): Record { const map: Record = {}; if (!sections) return map; for (const section of sections) { for (const item of section.items) { if (item.targetPage && item.title && !map[item.targetPage]) { map[item.targetPage] = item.title; } } } return map; } const EXTERNAL_PAGE_PREFIX = '__external__'; const EXTERNAL_PAGE_SEPARATOR = '::'; function safeDecodeURIComponent(raw: string): string { try { return decodeURIComponent(raw); } catch { return raw; } } function makeExternalPageId(title: string, key?: string) { return `${EXTERNAL_PAGE_PREFIX}${encodeURIComponent(title)}${EXTERNAL_PAGE_SEPARATOR}${encodeURIComponent( key || '' )}`; } /** Generic preference screen renderer - driven by page data from config */ export const PreferenceScreenPage: React.FC = () => { const { pageId } = useParams<{ pageId: string }>(); const { bindTap, go } = useSettingsGestures(); const { s, t } = useSettingsStrings(); const data = useSettingsStore(selectPagesData); const loading = useSettingsStore(selectPagesLoading); const error = useSettingsStore(selectPagesError); const mainSections = data?.mainSections ?? null; const pages = data?.pages ?? null; const mainTargetPageTitleMap = useMemo(() => buildMainTargetPageTitleMap(mainSections), [mainSections]); const [toast, setToast] = useState<{ visible: boolean; message: string }>({ visible: false, message: '', }); const toastTimerRef = useRef(undefined); const showToast = (message: string) => { setToast({ visible: true, message }); if (toastTimerRef.current) { window.clearTimeout(toastTimerRef.current); } toastTimerRef.current = window.setTimeout(() => { setToast((prev) => ({ ...prev, visible: false })); }, 1600); }; useEffect(() => { return () => { if (toastTimerRef.current) window.clearTimeout(toastTimerRef.current); }; }, []); // Keep info pages reactive: re-render when OS/device state changes. // NOTE: Must stay before any early return to keep hook order stable. useSyncExternalStore( subscribeOsDataRevision, getOsDataRevision ); // ── External/system pages stub ───────────────────────────────────── if (pageId?.startsWith(EXTERNAL_PAGE_PREFIX)) { const raw = pageId.slice(EXTERNAL_PAGE_PREFIX.length); const [encTitle = '', encKey = ''] = raw.split(EXTERNAL_PAGE_SEPARATOR); const title = safeDecodeURIComponent(encTitle) || s.settings; const key = safeDecodeURIComponent(encKey || ''); return (
{t(title)}
{s.on_a_real_device_this_would_open_a_system_page}{key ? `(${key})` : ''}{s.simulation_not_supported_suffix}
); } // ── Dynamic (programmatic) pages that exist on device but aren't pure XML ── // NOTE: Keep this AFTER hooks to avoid conditional hook calls across pageId changes. if (pageId === 'wifi_settings' || pageId === 'wifi_settings2') { return ; } if (pageId === 'bluetooth_settings' || pageId === 'bluetooth_switchbar_screen') { return ; } if (pageId === 'storage_dashboard_fragment') { return ; } if (pageId === 'launcher_settings') { return ; } if (pageId === 'silent_mode_settings') { return ; } if (pageId === 'locale_picker') { return ; } if (pageId === 'notification_managing') { return ; } if (pageId === 'permission_managing') { return ; } if (pageId?.startsWith('app_permission_detail__')) { const raw = pageId.slice('app_permission_detail__'.length); let appId = raw; try { appId = decodeURIComponent(raw); } catch { // ignore } return ; } if (pageId?.startsWith('notification_app__')) { const raw = pageId.slice('notification_app__'.length); let appId = raw; try { appId = decodeURIComponent(raw); } catch { // ignore } return ; } if (pageId === 'saved_access_points' || pageId === 'wifi_display_saved_access_points2' || pageId === 'saved_wifi') { return ; } if (pageId?.startsWith('wifi_saved_network__')) { const raw = pageId.slice('wifi_saved_network__'.length); let ssid = raw; try { ssid = decodeURIComponent(raw); } catch { // ignore } return ; } if (!pages) { return (
{s.settings}
{error ? s.settings_data_load_failed : (loading ? s.loading_settings_data : s.settings_data_not_ready)}
); } const page = pageId ? findPage(pageId, pages) : undefined; if (!page) { // Fallback: show a stub page with the id as title const displayTitle = pageId ? pageId.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) : s.settings; return (
{displayTitle}
{s.this_page_content_has_not_loaded_yet}
); } const getTargetPageForItem = (item: SettingsItem): string | undefined => { if (item.targetPage) return item.targetPage; // Heuristics: some pages are wired in code (no fragment/intent in XML) if (page.id === 'accessibility_text_reading_options') { if (item.key === 'font_size' || item.key === 'display_size') return 'PageLayoutFragment'; } if (page.id === 'display_settings_screen') { if (item.key === 'font_settings') return 'font_settings2'; if (item.key === 'page_layout_settings') return 'PageLayoutFragment'; } if (page.id === 'my_device_settings') { if (item.key === 'storage_settings') return 'storage_dashboard_fragment'; } if (page.id === 'my_device_detail_settings') { if (item.key === 'device_internal_memory') return 'storage_dashboard_fragment'; } if (page.id === 'my_device_info_pref_screen') { if (item.key === 'sim_status') return 'device_info_sim_status'; } if (page.id === 'notification_status_bar_settings') { if (item.key === 'notification_managing') return 'notification_managing'; } if ( item.key === 'permission_manager' || item.key === 'permission_settings' || item.key === 'permission_manage' || item.key === 'privacy_authorize_revoke' ) { return 'permission_managing'; } if (page.id === 'wifi_settings' || page.id === 'wifi_settings2') { if (item.key === 'saved_access_point' || item.key === 'saved_access_points' || item.key === 'saved_networks') { return 'saved_access_points'; } } // Generic fallback: many preference keys map to an internal page id. if (item.key) { if (findPage(item.key, pages)) return item.key; if (findPage(`${item.key}_settings`, pages)) return `${item.key}_settings`; if (findPage(`${item.key}_screen`, pages)) return `${item.key}_screen`; } return undefined; }; const handleItemClick = (item: SettingsItem) => { if (item.type === 'preference' && item.key && THEME_STORE_KEYS.has(item.key)) { const os = window.__OS__; if (os && typeof os.openApp === 'function') { os.openApp('theme_store'); return; } showToast(s.theme_store_unavailable); return; } const target = getTargetPageForItem(item); if (target) { go('page.open', { pageId: target }); return; } if (item.type === 'preference') { go('page.open', { pageId: makeExternalPageId(item.title || s.settings, item.key || ''), }); } }; const renderItem = (item: SettingsItem, idx: number, total: number) => { const isLast = idx === total - 1; const settingKey = item.key || `${page.id}__${item.type}__${idx}`; const isInfoPage = INFO_VALUE_PAGES.has(page.id); const dynamicValue = isInfoPage && item.key ? formatValue(routeGetPreference(item.key), s) : undefined; const tTitle = item.title ? t(item.title) : item.title; const tSummary = item.summary ? t(item.summary) : item.summary; // Device name is editable on real phones. Use an input dialog instead of external stub/copy-only. if (page.id === 'my_device_settings' && item.key === 'device_name') { return ( ); } switch (item.type) { case 'switch': case 'checkbox': return ( ); case 'seekbar': return ( ); case 'list': { return ( ({ ...o, label: t(o.label) }))} defaultValue={item.defaultValue} settingKey={settingKey} showDivider={!isLast} onMissingOptions={() => showToast(s.this_setting_has_no_available_options_may_be)} /> ); } case 'value': { const target = getTargetPageForItem(item); // For device information pages, most \"value\" rows are read-only info. // If a target exists, treat it as navigation (keep ValuePreference behavior). if (isInfoPage && !target && item.key) { return ( { if (!dynamicValue) return; ClipboardService.copyText(dynamicValue, 'settings'); showToast(s.copied_to_clipboard); }} /> ); } return ( go('page.open', { pageId: target }) : undefined} itemProps={ target ? bindTap('page.open', { params: { pageId: target } }) : undefined } /> ); } case 'footer': return (
{tTitle || tSummary}
); default: { const target = getTargetPageForItem(item); const hasInfoValue = isInfoPage && !!item.key && !!dynamicValue; const shouldCopyValue = hasInfoValue && !target; // Filter out "null" or placeholder summaries (resource keys / placeholders) const cleanSummary = (() => { const s = item.summary?.trim(); if (!s) return undefined; if (s === 'null') return undefined; if (shouldCopyValue && s === t('无法获取')) return undefined; if (s.includes('%s')) return undefined; if (s === 'summary_placeholder') return undefined; if (/^[a-z0-9_]+$/i.test(s) && s.includes('_')) return undefined; return t(s); })(); return ( { if (shouldCopyValue) { ClipboardService.copyText(dynamicValue!, 'settings'); showToast(s.copied_to_clipboard); return; } handleItemClick(item); }} itemProps={ target ? bindTap('page.open', { params: { pageId: target }, onTrigger: () => handleItemClick(item), }) : undefined } /> ); } } }; const displayTitle = t( mainTargetPageTitleMap[page.id] || page.title || page.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) ); const isPlaceholderText = (raw: string | undefined) => { const s = raw?.trim(); if (!s) return true; if (s === 'null') return true; if (s === 'summary_placeholder') return true; // Many extracted pages contain resource keys as titles (e.g. "foo_bar_baz"). if (/^[a-z0-9_]+$/i.test(s) && s.includes('_')) return true; return false; }; return (
{page.categories.map((category, catIdx) => { const visibleItems = category.items.filter((item) => { if (item.type === 'footer') { return !(isPlaceholderText(item.title) && isPlaceholderText(item.summary)); } return !isPlaceholderText(item.title); }); if (!visibleItems.length) return null; return ( {visibleItems.map((item, idx) => renderItem(item, idx, visibleItems.length) )} ); })}
); };