Performance improvements and navigation fixes

Performance fixes for library, artist screens

Navigational fixes in the player

Item Context optimization
This commit is contained in:
Violet Caulfield
2025-08-22 05:26:38 -05:00
parent 43240188f2
commit c8f18d37ca
10 changed files with 67 additions and 81 deletions

View File

@@ -2719,7 +2719,7 @@ PODS:
- RNWorklets
- SocketRocket
- Yoga
- RNScreens (4.15.0):
- RNScreens (4.15.2):
- boost
- DoubleConversion
- fast_float
@@ -2746,10 +2746,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNScreens/common (= 4.15.0)
- RNScreens/common (= 4.15.2)
- SocketRocket
- Yoga
- RNScreens/common (4.15.0):
- RNScreens/common (4.15.2):
- boost
- DoubleConversion
- fast_float
@@ -3306,7 +3306,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
RNReanimated: ee96d03fe3713993a30cc205522792b4cb08e4f9
RNScreens: 48bbaca97a5f9aedc3e52bd48673efd2b6aac4f6
RNScreens: 8d88d38778e35ce95abeb228d3b5ea0c6e635cad
RNSentry: 95e1ed0ede28a4af58aaafedeac9fcfaba0e89ce
RNWorklets: e8335dff9d27004709f58316985769040cd1e8f2
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d

View File

@@ -82,7 +82,7 @@
"react-native-pager-view": "^7.0.0",
"react-native-reanimated": "4.0.2",
"react-native-safe-area-context": "^5.6.0",
"react-native-screens": "4.15.0",
"react-native-screens": "4.15.2",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.15.0",
"react-native-toast-message": "^2.3.3",

View File

@@ -54,6 +54,7 @@ export default function SimilarArtists(): React.JSX.Element {
)
}
onScroll={scrollHandler}
removeClippedSubviews
/>
)
}

View File

@@ -2,7 +2,6 @@ import TextTicker from 'react-native-text-ticker'
import { getToken, XStack, YStack } from 'tamagui'
import { TextTickerConfig } from '../component.config'
import { Text } from '../../Global/helpers/text'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React, { useCallback, useMemo, memo } from 'react'
import ItemImage from '../../Global/components/image'
import { useQuery } from '@tanstack/react-query'
@@ -10,17 +9,13 @@ import { fetchItem } from '../../../api/queries/item'
import { useJellifyContext } from '../../../providers'
import FavoriteButton from '../../Global/components/favorite-button'
import { QueryKeys } from '../../../enums/query-keys'
import { PlayerParamList } from '../../../screens/Player/types'
import { useNowPlayingContext } from '../../../providers/Player'
import navigationRef from '../../../../navigation'
import Icon from '../../Global/components/icon'
import { getItemName } from '../../../utils/text'
import { CommonActions } from '@react-navigation/native'
interface SongInfoProps {
navigation: NativeStackNavigationProp<PlayerParamList>
}
function SongInfo({ navigation }: SongInfoProps): React.JSX.Element {
export default function SongInfo(): React.JSX.Element {
const { api } = useJellifyContext()
const nowPlaying = useNowPlayingContext()
@@ -43,39 +38,25 @@ function SongInfo({ navigation }: SongInfoProps): React.JSX.Element {
// Memoize navigation handlers
const handleAlbumPress = useCallback(() => {
if (album) {
navigation.goBack() // Dismiss player modal
navigationRef.navigate('Tabs', {
screen: 'LibraryTab',
params: {
screen: 'Album',
params: {
album,
},
},
})
navigationRef.goBack() // Dismiss player modal
navigationRef.dispatch(CommonActions.navigate('Album', { album }))
}
}, [album, navigation])
}, [album])
const handleArtistPress = useCallback(() => {
if (artistItems) {
if (artistItems.length > 1) {
navigation.navigate('MultipleArtistsSheet', {
artists: artistItems,
})
navigationRef.dispatch(
CommonActions.navigate('MultipleArtistsSheet', {
artists: artistItems,
}),
)
} else {
navigation.goBack() // Dismiss player modal
navigationRef.navigate('Tabs', {
screen: 'LibraryTab',
params: {
screen: 'Artist',
params: {
artist: artistItems[0],
},
},
})
navigationRef.goBack() // Dismiss player modal
navigationRef.dispatch(CommonActions.navigate('Artist', { artist: artistItems[0] }))
}
}
}, [artistItems, navigation])
}, [artistItems])
return (
<XStack>
@@ -108,9 +89,3 @@ function SongInfo({ navigation }: SongInfoProps): React.JSX.Element {
</XStack>
)
}
// Memoize the component to prevent unnecessary re-renders
export default memo(SongInfo, (prevProps: SongInfoProps, nextProps: SongInfoProps) => {
// Only re-render if navigation changes (which it shouldn't)
return prevProps.navigation === nextProps.navigation
})

View File

@@ -1,16 +1,7 @@
import { useNowPlayingContext } from '../../providers/Player'
import React, { useCallback, useMemo, useState } from 'react'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import {
YStack,
XStack,
getToken,
useTheme,
ZStack,
useWindowDimensions,
View,
getTokenValue,
} from 'tamagui'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { YStack, useTheme, ZStack, useWindowDimensions, View, getTokenValue } from 'tamagui'
import Scrubber from './components/scrubber'
import Controls from './components/controls'
import Toast from 'react-native-toast-message'
@@ -21,15 +12,9 @@ import BlurredBackground from './components/blurred-background'
import PlayerHeader from './components/header'
import SongInfo from './components/song-info'
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { PlayerParamList } from '../../screens/Player/types'
import { Platform } from 'react-native'
export default function PlayerScreen({
navigation,
}: {
navigation: NativeStackNavigationProp<PlayerParamList>
}): React.JSX.Element {
export default function PlayerScreen(): React.JSX.Element {
const performanceMetrics = usePerformanceMonitor('PlayerScreen', 5)
const [showToast, setShowToast] = useState(true)
@@ -84,7 +69,7 @@ export default function PlayerScreen({
<PlayerHeader />
<YStack justifyContent='flex-start' gap={'$4'} flexShrink={1}>
<SongInfo navigation={navigation} />
<SongInfo />
<Scrubber />
{/* playback progress goes here */}

View File

@@ -1,5 +1,5 @@
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
import { createContext, ReactNode, useEffect } from 'react'
import { createContext, ReactNode, useEffect, useRef } from 'react'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../enums/query-keys'
import { fetchMediaInfo } from '../../api/queries/media'
@@ -11,6 +11,7 @@ import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { ItemArtistProvider } from './item-artists'
import { queryClient } from '../../constants/query-client'
import { fetchUserData } from '../../api/queries/favorites'
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
interface ItemContext {
item: BaseItemDto
@@ -43,6 +44,8 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.
item,
children,
}) => {
const perfMonitor = usePerformanceMonitor('ItemProvider', 5)
const { api, user } = useJellifyContext()
const streamingQuality = useStreamingQualityContext()
@@ -51,9 +54,20 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.
const artistIds = ArtistItems?.map(({ Id }) => Id) ?? []
const prefetchedContext = useRef<Record<string, true>>({})
useEffect(() => {
// Fail fast if we don't have an Item ID to work with
if (!Id) return
const effectSig = `${Id}-${Type}`
// If we've already warmed the cache for this item, return
if (prefetchedContext.current[effectSig]) return
prefetchedContext.current[effectSig] = true
console.debug(`Warming context query cache for item ${Id}`)
/**
* Fetch and cache the media sources if this item is a track
*/
@@ -123,7 +137,7 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.
queryKey: [QueryKeys.UserData, Id],
queryFn: () => fetchUserData(api, user, Id),
})
}, [queryClient, api, user, Id, Type, AlbumId, UserData, item, streamingQuality])
}, [queryClient, api?.basePath, user?.id, Id, streamingQuality])
return (
<ItemContext.Provider value={{ item }}>

View File

@@ -1,4 +1,4 @@
import { createContext, useEffect } from 'react'
import { createContext, useEffect, useRef } from 'react'
import { useJellifyContext } from '..'
import { QueryKeys } from '../../enums/query-keys'
import { fetchItem } from '../../api/queries/item'
@@ -19,16 +19,28 @@ export const ItemArtistProvider: ({
}) => React.JSX.Element = ({ artistId }) => {
const { api } = useJellifyContext()
const prefetchedContext = useRef<Record<string, true>>({})
useEffect(() => {
// Fail fast if we don't have an artist ID to work with
if (!artistId) return
const effectSig = artistId
// If we've already warmed the cache for this artist, return
if (prefetchedContext.current[effectSig]) return
prefetchedContext.current[effectSig] = true
console.debug(`Warming context cache for artist ${artistId}`)
/**
* Store queryable of artist item
*/
if (artistId)
queryClient.ensureQueryData({
queryKey: [QueryKeys.ArtistById, artistId],
queryFn: () => fetchItem(api, artistId!),
})
})
queryClient.ensureQueryData({
queryKey: [QueryKeys.ArtistById, artistId],
queryFn: () => fetchItem(api, artistId!),
})
}, [api, artistId])
return <ItemArtistContext.Provider value={{ artistId }} />
}

View File

@@ -348,11 +348,6 @@ const QueueContextInitailizer = () => {
console.debug(
`Queued ${queue.length} tracks, starting at ${finalStartIndex}${shuffleQueue ? ' (shuffled)' : ''}`,
)
// Set skipping to false after a short delay to prevent flickering
// IDK why this needs to be 1000ms, but there are a lot of events are emitted
// by RNTP at this time so we need to wait for it to settle
setTimeout(() => setSkipping(false), 1000)
}
/**
@@ -547,6 +542,10 @@ const QueueContextInitailizer = () => {
trigger('notificationSuccess')
console.debug(`Loaded new queue`)
// Set skipping to false after a short delay to prevent flickering
// IDK why this needs to be 500ms, but there are a lot of events are emitted
// by RNTP at this time so we need to wait for it to settle
setTimeout(() => setSkipping(false), 500)
if (startPlayback) await TrackPlayer.play()
},
onError: async (error: Error) => {

View File

@@ -27,6 +27,7 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element
animation: 'shift',
tabBarActiveTintColor: theme.primary.val,
tabBarInactiveTintColor: theme.neutral.val,
lazy: true,
}}
tabBar={(props) => (
<>
@@ -63,7 +64,6 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element
<MaterialDesignIcons name='music-box-multiple' color={color} size={size} />
),
tabBarButtonTestID: 'library-tab-button',
lazy: false, // Load on mount since we need to be able to navigate here from the player
}}
/>

View File

@@ -8523,10 +8523,10 @@ react-native-safe-area-context@^5.6.0:
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.0.tgz#0ab284c291bb57d59330abf7dfe65156d6340e78"
integrity sha512-tJas3YOdsuCg3kepCTGF3LWZp9onMbb9Agju2xfs2kRX8d/5TMUPmupBpjerk/B7Tv/zeJnk+qp5neA96Y0otQ==
react-native-screens@4.15.0:
version "4.15.0"
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.15.0.tgz#8b34056b72a2c92da4de2f77419a116154c88e81"
integrity sha512-LPz+9qWDfwY3pPKojrWPcQnb2sAq9cy/qA8ZAS14ksSdzqFkTTUbs1as2WGBo7xBtdx5Ht78bF9nNh8libX1Xw==
react-native-screens@4.15.2:
version "4.15.2"
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.15.2.tgz#93ba015f5167a2fb5e2e2b71807f083ec8ed1ef2"
integrity sha512-RA9fUT/5OTPJ2ML3BNUbe8UtfcU7iufE+r9sLr/IesFdBDj38bZiqmH88iorI5Vfgp7O3zf2aK390Tbkfp9Xfw==
dependencies:
react-freeze "^1.0.0"
react-native-is-edge-to-edge "^1.2.1"