mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-21 13:30:11 -06:00
refactor: enhance settings tab components with lazy loading and improved structure (#783)
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user