refactor: enhance settings tab components with lazy loading and improved structure (#783)

This commit is contained in:
skalthoff
2025-12-05 18:33:10 -08:00
committed by GitHub
parent aacc675b31
commit dfd348a67e
4 changed files with 223 additions and 151 deletions

View File

@@ -1,16 +1,66 @@
import React from 'react'
import React, { Suspense, lazy } from 'react'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import { getToken, getTokenValue, useTheme } from 'tamagui'
import AccountTab from './components/account-tab'
import Icon from '../Global/components/icon'
import PreferencesTab from './components/preferences-tab'
import PlaybackTab from './components/playback-tab'
import InfoTab from './components/info-tab'
import { getToken, getTokenValue, useTheme, Spinner, YStack } from 'tamagui'
import SettingsTabBar from './tab-bar'
import StorageTab from './components/usage-tab'
import { SafeAreaView } from 'react-native-safe-area-context'
// Lazy load tab components to improve initial render
const PreferencesTab = lazy(() => import('./components/preferences-tab'))
const PlaybackTab = lazy(() => import('./components/playback-tab'))
const StorageTab = lazy(() => import('./components/usage-tab'))
const AccountTab = lazy(() => import('./components/account-tab'))
const InfoTab = lazy(() => import('./components/info-tab'))
const SettingsTabsNavigator = createMaterialTopTabNavigator()
function TabFallback() {
return (
<YStack flex={1} alignItems='center' justifyContent='center' backgroundColor='$background'>
<Spinner size='large' color='$primary' />
</YStack>
)
}
// Wrap lazy components with Suspense
function LazyPreferencesTab() {
return (
<Suspense fallback={<TabFallback />}>
<PreferencesTab />
</Suspense>
)
}
function LazyPlaybackTab() {
return (
<Suspense fallback={<TabFallback />}>
<PlaybackTab />
</Suspense>
)
}
function LazyStorageTab() {
return (
<Suspense fallback={<TabFallback />}>
<StorageTab />
</Suspense>
)
}
function LazyAccountTab() {
return (
<Suspense fallback={<TabFallback />}>
<AccountTab />
</Suspense>
)
}
function LazyInfoTab() {
return (
<Suspense fallback={<TabFallback />}>
<InfoTab />
</Suspense>
)
}
export default function Settings(): React.JSX.Element {
const theme = useTheme()
@@ -30,13 +80,14 @@ export default function Settings(): React.JSX.Element {
fontFamily: 'Figtree-Bold',
},
tabBarPressOpacity: 0.5,
lazy: true, // Enable lazy loading to prevent all tabs from mounting simultaneously
lazy: true,
lazyPreloadDistance: 0, // Only load the active tab
}}
tabBar={(props) => <SettingsTabBar {...props} />}
>
<SettingsTabsNavigator.Screen
name='Settings'
component={PreferencesTab}
component={LazyPreferencesTab}
options={{
title: 'App',
}}
@@ -44,17 +95,17 @@ export default function Settings(): React.JSX.Element {
<SettingsTabsNavigator.Screen
name='Playback'
component={PlaybackTab}
component={LazyPlaybackTab}
options={{
title: 'Player',
}}
/>
<SettingsTabsNavigator.Screen name='Usage' component={StorageTab} />
<SettingsTabsNavigator.Screen name='Usage' component={LazyStorageTab} />
<SettingsTabsNavigator.Screen name='User' component={AccountTab} />
<SettingsTabsNavigator.Screen name='User' component={LazyAccountTab} />
<SettingsTabsNavigator.Screen name='About' component={InfoTab} />
<SettingsTabsNavigator.Screen name='About' component={LazyInfoTab} />
{/*
<SettingsTabsNavigator.Screen
name='Labs'

View File

@@ -9,9 +9,25 @@ import { useQuery } from '@tanstack/react-query'
import { INFO_CAPTIONS } from '../../../configs/info.config'
import { ONE_HOUR } from '../../../constants/query-client'
import { pickRandomItemFromArray } from '../../../utils/random'
import { FlashList } from '@shopify/flash-list'
import { getStoredOtaVersion } from 'react-native-nitro-ota'
import { downloadUpdate } from '../../OtaUpdates'
function PatronsList({ patrons }: { patrons: { fullName: string }[] | undefined }) {
if (!patrons?.length) return null
return (
<XStack flexWrap='wrap' gap='$2' marginTop='$2'>
{patrons.map((patron, index) => (
<XStack key={index} alignItems='flex-start' maxWidth={'$20'}>
<Text numberOfLines={1} lineBreakStrategyIOS='standard'>
{patron.fullName}
</Text>
</XStack>
))}
</XStack>
)
}
export default function InfoTab() {
const patrons = usePatrons()
@@ -101,47 +117,37 @@ export default function InfoTab() {
iconName: 'hand-heart',
iconColor: '$secondary',
children: (
<FlashList
data={patrons}
ListHeaderComponent={
<YStack>
<XStack
justifyContent='flex-start'
gap={'$4'}
marginVertical={'$2'}
>
<XStack
justifyContent='flex-start'
gap={'$4'}
marginVertical={'$2'}
alignItems='center'
onPress={() =>
Linking.openURL(
'https://github.com/sponsors/anultravioletaurora/',
)
}
>
<XStack
alignItems='center'
onPress={() =>
Linking.openURL(
'https://github.com/sponsors/anultravioletaurora/',
)
}
>
<Icon name='github' small color='$borderColor' />
<Text>Sponsors</Text>
</XStack>
<XStack
alignItems='center'
onPress={() =>
Linking.openURL(
'https://patreon.com/anultravioletaurora',
)
}
>
<Icon name='patreon' small color='$borderColor' />
<Text>Patreon</Text>
</XStack>
<Icon name='github' small color='$borderColor' />
<Text>Sponsors</Text>
</XStack>
}
numColumns={2}
renderItem={({ item }) => (
<XStack alignItems='flex-start' maxWidth={'$20'}>
<Text numberOfLines={1} lineBreakStrategyIOS='standard'>
{item.fullName}
</Text>
<XStack
alignItems='center'
onPress={() =>
Linking.openURL(
'https://patreon.com/anultravioletaurora',
)
}
>
<Icon name='patreon' small color='$borderColor' />
<Text>Patreon</Text>
</XStack>
)}
/>
</XStack>
<PatronsList patrons={patrons} />
</YStack>
),
},
]}

View File

@@ -9,7 +9,6 @@ import {
useThemeSetting,
} from '../../../stores/settings/app'
import { useSwipeSettingsStore } from '../../../stores/settings/swipe'
import { useMemo } from 'react'
import Button from '../../Global/helpers/button'
import Icon from '../../Global/components/icon'
@@ -42,31 +41,20 @@ const THEME_OPTIONS: ThemeOptionConfig[] = [
},
]
export default function PreferencesTab(): React.JSX.Element {
const [sendMetrics, setSendMetrics] = useSendMetricsSetting()
const [reducedHaptics, setReducedHaptics] = useReducedHapticsSetting()
const [themeSetting, setThemeSetting] = useThemeSetting()
const [hideRunTimes, setHideRunTimes] = useHideRunTimesSetting()
const left = useSwipeSettingsStore((s) => s.left)
const right = useSwipeSettingsStore((s) => s.right)
const toggleLeft = useSwipeSettingsStore((s) => s.toggleLeft)
const toggleRight = useSwipeSettingsStore((s) => s.toggleRight)
const ActionChip = ({
active,
label,
icon,
onPress,
testID,
}: {
active: boolean
label: string
icon: string
onPress: () => void
testID?: string
}) => (
function ActionChip({
active,
label,
icon,
onPress,
testID,
}: {
active: boolean
label: string
icon: string
onPress: () => void
testID?: string
}) {
return (
<Button
testID={testID}
pressStyle={{
@@ -87,19 +75,69 @@ export default function PreferencesTab(): React.JSX.Element {
</SizableText>
</Button>
)
}
const themeSubtitle = useMemo(() => {
switch (themeSetting) {
case 'light':
return 'You crazy diamond'
case 'dark':
return "There's a dark side??"
case 'oled':
return 'Back in black'
default:
return "I'm down with this system"
}
}, [themeSetting])
function ThemeOptionCard({
option,
isSelected,
onPress,
}: {
option: ThemeOptionConfig
isSelected: boolean
onPress: () => void
}) {
return (
<YStack
onPress={onPress}
pressStyle={{ scale: 0.97 }}
animation='quick'
borderWidth={'$1'}
borderColor={isSelected ? '$primary' : '$borderColor'}
backgroundColor={isSelected ? '$background25' : '$background'}
borderRadius={'$9'}
padding='$3'
gap='$2'
hitSlop={8}
accessibilityRole='button'
accessibilityLabel={`${option.label} theme option`}
accessibilityState={{ selected: isSelected }}
>
<XStack alignItems='center' gap='$2'>
<Icon small name={option.icon} color={isSelected ? '$primary' : '$borderColor'} />
<SizableText size={'$4'} flex={1} fontWeight='600'>
{option.label}
</SizableText>
{isSelected && <Icon small name='check-circle-outline' color={'$primary'} />}
</XStack>
</YStack>
)
}
function getThemeSubtitle(themeSetting: ThemeSetting): string {
switch (themeSetting) {
case 'light':
return 'You crazy diamond'
case 'dark':
return "There's a dark side??"
case 'oled':
return 'Back in black'
default:
return "I'm down with this system"
}
}
export default function PreferencesTab(): React.JSX.Element {
const [sendMetrics, setSendMetrics] = useSendMetricsSetting()
const [reducedHaptics, setReducedHaptics] = useReducedHapticsSetting()
const [themeSetting, setThemeSetting] = useThemeSetting()
const [hideRunTimes, setHideRunTimes] = useHideRunTimesSetting()
const left = useSwipeSettingsStore((s) => s.left)
const right = useSwipeSettingsStore((s) => s.right)
const toggleLeft = useSwipeSettingsStore((s) => s.toggleLeft)
const toggleRight = useSwipeSettingsStore((s) => s.toggleRight)
const themeSubtitle = getThemeSubtitle(themeSetting)
return (
<SettingsListGroup
@@ -242,39 +280,3 @@ export default function PreferencesTab(): React.JSX.Element {
/>
)
}
function ThemeOptionCard({
option,
isSelected,
onPress,
}: {
option: ThemeOptionConfig
isSelected: boolean
onPress: () => void
}) {
return (
<YStack
onPress={onPress}
pressStyle={{ scale: 0.97 }}
animation='quick'
borderWidth={'$1'}
borderColor={isSelected ? '$primary' : '$borderColor'}
backgroundColor={isSelected ? '$background25' : '$background'}
borderRadius={'$9'}
padding='$3'
gap='$2'
hitSlop={8}
accessibilityRole='button'
accessibilityLabel={`${option.label} theme option`}
accessibilityState={{ selected: isSelected }}
>
<XStack alignItems='center' gap='$2'>
<Icon small name={option.icon} color={isSelected ? '$primary' : '$borderColor'} />
<SizableText size={'$4'} flex={1} fontWeight='600'>
{option.label}
</SizableText>
{isSelected && <Icon small name='check-circle-outline' color={'$primary'} />}
</XStack>
</YStack>
)
}

View File

@@ -11,6 +11,38 @@ interface SettingsListGroupProps {
borderColor?: ThemeTokens
}
function SettingsListItem({
setting,
isLast,
}: {
setting: SettingsTabList[number]
isLast: boolean
}) {
return (
<>
<YGroup.Item>
<ListItem
size={'$5'}
title={setting.title}
icon={<Icon name={setting.iconName} color={setting.iconColor} />}
subTitle={
setting.subTitle && <Text color={'$borderColor'}>{setting.subTitle}</Text>
}
onPress={setting.onPress}
iconAfter={
setting.onPress ? (
<Icon name='chevron-right' color={'$borderColor'} />
) : undefined
}
>
{setting.children}
</ListItem>
</YGroup.Item>
{!isLast && <Separator />}
</>
)
}
export default function SettingsListGroup({
settingsList,
borderColor,
@@ -25,30 +57,11 @@ export default function SettingsListGroup({
margin={'$3'}
>
{settingsList.map((setting, index, self) => (
<>
<YGroup.Item key={setting.title}>
<ListItem
size={'$5'}
title={setting.title}
icon={<Icon name={setting.iconName} color={setting.iconColor} />}
subTitle={
setting.subTitle && (
<Text color={'$borderColor'}>{setting.subTitle}</Text>
)
}
onPress={setting.onPress}
iconAfter={
setting.onPress ? (
<Icon name='chevron-right' color={'$borderColor'} />
) : undefined
}
>
{setting.children}
</ListItem>
</YGroup.Item>
{index !== self.length - 1 && <Separator />}
</>
<SettingsListItem
key={setting.title}
setting={setting}
isLast={index === self.length - 1}
/>
))}
</YGroup>
{footer}