mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-07 03:20:19 -06:00
performance improvements related to background data fetching
This commit is contained in:
committed by
Violet Caulfield
parent
e8be30499e
commit
7ab462b201
@@ -1,14 +1,17 @@
|
||||
import { ActivityIndicator, RefreshControl } from 'react-native'
|
||||
import { getToken, Separator, XStack, YStack } from 'tamagui'
|
||||
import React from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import { FlashList, ViewToken } from '@shopify/flash-list'
|
||||
import { FetchNextPageOptions } from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import LibraryStackParamList from '../../screens/Library/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { warmItemContext } from '../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { useStreamingQualityContext } from '../../providers/Settings'
|
||||
|
||||
interface AlbumsProps {
|
||||
albums: (string | number | BaseItemDto)[] | undefined
|
||||
@@ -28,6 +31,19 @@ export default function Albums({
|
||||
}: AlbumsProps): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<string | number | BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable && typeof item === 'object')
|
||||
warmItemContext(api, user, item, streamingQuality)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
|
||||
const stickyHeaderIndices = React.useMemo(() => {
|
||||
if (!showAlphabeticalSelector || !albums) return []
|
||||
@@ -89,6 +105,7 @@ export default function Albums({
|
||||
refreshControl={<RefreshControl refreshing={isPending} />}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
</XStack>
|
||||
)
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { ArtistAlbumsProps, ArtistEpsProps, ArtistFeaturedOnProps } from './types'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { useArtistContext } from '../../providers/Artist'
|
||||
import { convertRunTimeTicksToSeconds } from '../../utils/runtimeticks'
|
||||
import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated'
|
||||
import { ActivityIndicator } from 'react-native'
|
||||
import { ActivityIndicator, ViewToken } from 'react-native'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { getToken } from 'tamagui'
|
||||
import navigationRef from '../../../navigation'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { warmItemContext } from '../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { useStreamingQualityContext } from '../../providers/Settings'
|
||||
export default function Albums({
|
||||
route,
|
||||
navigation,
|
||||
}: ArtistAlbumsProps | ArtistEpsProps | ArtistFeaturedOnProps): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
const { albums, fetchingAlbums, featuredOn, scroll } = useArtistContext()
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
@@ -22,6 +30,14 @@ export default function Albums({
|
||||
},
|
||||
})
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable) warmItemContext(api, user, item, streamingQuality)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const [columns, setColumns] = useState(Math.floor(width / getToken('$20')))
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,6 +113,7 @@ export default function Albums({
|
||||
)
|
||||
}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ import { ArtistsProps } from '../../screens/types'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import { useLibrarySortAndFilterContext } from '../../providers/Library'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
||||
import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list'
|
||||
import { AZScroller } from '../Global/components/alphabetical-selector'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { isString } from 'lodash'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import LibraryStackParamList from '../../screens/Library/types'
|
||||
import { warmItemContext } from '../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { useStreamingQualityContext } from '../../providers/Settings'
|
||||
|
||||
/**
|
||||
* @param artistsInfiniteQuery - The infinite query for artists
|
||||
@@ -27,6 +30,11 @@ export default function Artists({
|
||||
artistPageParams,
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
const { isFavorites } = useLibrarySortAndFilterContext()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
@@ -36,6 +44,15 @@ export default function Artists({
|
||||
|
||||
const pendingLetterRef = useRef<string | null>(null)
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<string | number | BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable && typeof item === 'object')
|
||||
warmItemContext(api, user, item, streamingQuality)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const alphabeticalSelectorCallback = async (letter: string) => {
|
||||
console.debug(`Alphabetical Selector Callback: ${letter}`)
|
||||
|
||||
@@ -169,6 +186,7 @@ export default function Artists({
|
||||
artistsInfiniteQuery.fetchNextPage()
|
||||
}}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && artistPageParams && (
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function Index(): React.JSX.Element {
|
||||
useDiscoverContext()
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { warmItemContext } from '@/src/hooks/use-item-context'
|
||||
import { useJellifyContext } from '@/src/providers'
|
||||
import { useStreamingQualityContext } from '@/src/providers/Settings'
|
||||
import { warmItemContext } from '../../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { useStreamingQualityContext } from '../../../providers/Settings'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { FlashList, FlashListProps, ViewToken } from '@shopify/flash-list'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
@@ -4,9 +4,6 @@ import { getToken, Card as TamaguiCard, View, YStack } from 'tamagui'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Text } from '../helpers/text'
|
||||
import ItemImage from './image'
|
||||
import { warmItemContext } from '../../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { useStreamingQualityContext } from '../../../providers/Settings'
|
||||
|
||||
interface CardProps extends TamaguiCardProps {
|
||||
caption?: string | null | undefined
|
||||
@@ -24,10 +21,6 @@ interface CardProps extends TamaguiCardProps {
|
||||
* @param props
|
||||
*/
|
||||
export function ItemCard(props: CardProps) {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
return (
|
||||
<View alignItems='center' margin={'$1.5'}>
|
||||
<TamaguiCard
|
||||
@@ -41,7 +34,6 @@ export function ItemCard(props: CardProps) {
|
||||
animation='bouncy'
|
||||
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
|
||||
pressStyle={props.onPress ? { scale: 0.875 } : {}}
|
||||
onPressIn={() => warmItemContext(api, user, props.item, streamingQuality)}
|
||||
{...props}
|
||||
>
|
||||
<TamaguiCard.Header></TamaguiCard.Header>
|
||||
|
||||
@@ -12,9 +12,6 @@ import { runOnJS } from 'react-native-reanimated'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '../../../screens/types'
|
||||
import { warmItemContext } from '../../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { useStreamingQualityContext } from '../../../providers/Settings'
|
||||
|
||||
interface ItemRowProps {
|
||||
item: BaseItemDto
|
||||
@@ -43,10 +40,6 @@ export default function ItemRow({
|
||||
}: ItemRowProps): React.JSX.Element {
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
const gestureCallback = () => {
|
||||
switch (item.Type) {
|
||||
case 'Audio': {
|
||||
@@ -77,7 +70,6 @@ export default function ItemRow({
|
||||
alignContent='center'
|
||||
minHeight={'$7'}
|
||||
width={'100%'}
|
||||
onPressIn={() => warmItemContext(api, user, item, streamingQuality)}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', {
|
||||
item,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { RefreshControl } from 'react-native-gesture-handler'
|
||||
import { Separator } from 'tamagui'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import { FlashList, ViewToken } from '@shopify/flash-list'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { FetchNextPageOptions } from '@tanstack/react-query'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { BaseStackParamList } from '@/src/screens/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useRef } from 'react'
|
||||
import { warmItemContext } from '../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { useStreamingQualityContext } from '../../providers/Settings'
|
||||
|
||||
export interface PlaylistsProps {
|
||||
canEdit?: boolean | undefined
|
||||
@@ -27,6 +31,19 @@ export default function Playlists({
|
||||
canEdit,
|
||||
}: PlaylistsProps): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable) warmItemContext(api, user, item, streamingQuality)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
@@ -51,6 +68,7 @@ export default function Playlists({
|
||||
}
|
||||
}}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import Track from '../Global/components/track'
|
||||
import { getTokens, Separator } from 'tamagui'
|
||||
import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
@@ -6,9 +6,12 @@ import { Queue } from '../../player/types/queue-item'
|
||||
import { useNetworkContext } from '../../providers/Network'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import { FlashList, ViewToken } from '@shopify/flash-list'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '@/src/screens/types'
|
||||
import { warmItemContext } from '../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { useStreamingQualityContext } from '../../providers/Settings'
|
||||
|
||||
interface TracksProps {
|
||||
tracks: (string | number | BaseItemDto)[] | undefined
|
||||
@@ -29,6 +32,9 @@ export default function Tracks({
|
||||
filterDownloaded,
|
||||
filterFavorites,
|
||||
}: TracksProps): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
const { downloadedTracks } = useNetworkContext()
|
||||
|
||||
// Memoize the expensive tracks processing to prevent memory leaks
|
||||
@@ -73,6 +79,14 @@ export default function Tracks({
|
||||
[tracksToDisplay, queue],
|
||||
)
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable) warmItemContext(api, user, item, streamingQuality)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
|
||||
@@ -32,25 +32,6 @@ export default function useItemContext(item: BaseItemDto): void {
|
||||
}, [api, user, streamingQuality])
|
||||
}
|
||||
|
||||
export function warmArtistContext(api: Api | undefined, artistId: string): void {
|
||||
// Fail fast if we don't have an artist ID to work with
|
||||
if (!artistId) return
|
||||
|
||||
const queryKey = [QueryKeys.ArtistById, artistId]
|
||||
|
||||
// Bail out if we have data
|
||||
if (queryClient.getQueryState(queryKey)?.status === 'success') return
|
||||
|
||||
console.debug(`Warming context cache for artist ${artistId}`)
|
||||
/**
|
||||
* Store queryable of artist item
|
||||
*/
|
||||
queryClient.ensureQueryData({
|
||||
queryKey,
|
||||
queryFn: () => fetchItem(api, artistId!),
|
||||
})
|
||||
}
|
||||
|
||||
export function warmItemContext(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
@@ -64,38 +45,12 @@ export function warmItemContext(
|
||||
|
||||
console.debug(`Warming context query cache for item ${Id}`)
|
||||
|
||||
if (Type === BaseItemKind.Audio) {
|
||||
const mediaSourcesQueryKey = [QueryKeys.MediaSources, streamingQuality, Id]
|
||||
|
||||
if (queryClient.getQueryState(mediaSourcesQueryKey)?.status !== 'success')
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: mediaSourcesQueryKey,
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), Id),
|
||||
})
|
||||
|
||||
const albumQueryKey = [QueryKeys.Album, AlbumId]
|
||||
|
||||
if (AlbumId)
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: albumQueryKey,
|
||||
queryFn: () => fetchItem(api, AlbumId!),
|
||||
})
|
||||
}
|
||||
if (Type === BaseItemKind.Audio) warmTrackContext(api, user, item, streamingQuality)
|
||||
|
||||
if (Type === BaseItemKind.MusicArtist)
|
||||
queryClient.setQueryData([QueryKeys.ArtistById, Id], item)
|
||||
|
||||
if (Type === BaseItemKind.MusicAlbum) {
|
||||
queryClient.setQueryData([QueryKeys.Album, Id], item)
|
||||
|
||||
const albumDiscsQueryKey = [QueryKeys.ItemTracks, Id]
|
||||
|
||||
if (queryClient.getQueryState(albumDiscsQueryKey)?.status !== 'success')
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: albumDiscsQueryKey,
|
||||
queryFn: () => fetchAlbumDiscs(api, item),
|
||||
})
|
||||
}
|
||||
if (Type === BaseItemKind.MusicAlbum) warmAlbumContext(api, item)
|
||||
|
||||
/**
|
||||
* Prefetch query for a playlist's tracks
|
||||
@@ -114,10 +69,73 @@ export function warmItemContext(
|
||||
}),
|
||||
})
|
||||
|
||||
if (UserData) queryClient.setQueryData([QueryKeys.UserData, Id], UserData)
|
||||
else
|
||||
const userDataQueryKey = [QueryKeys.UserData, Id]
|
||||
if (queryClient.getQueryState(userDataQueryKey)?.status !== 'success') {
|
||||
if (UserData) queryClient.setQueryData([QueryKeys.UserData, Id], UserData)
|
||||
else
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [],
|
||||
queryFn: () => fetchUserData(api, user, Id),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function warmAlbumContext(api: Api | undefined, album: BaseItemDto): void {
|
||||
const { Id } = album
|
||||
|
||||
queryClient.setQueryData([QueryKeys.Album, Id], album)
|
||||
|
||||
const albumDiscsQueryKey = [QueryKeys.ItemTracks, Id]
|
||||
|
||||
if (queryClient.getQueryState(albumDiscsQueryKey)?.status !== 'success')
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [QueryKeys.UserData, Id],
|
||||
queryFn: () => fetchUserData(api, user, Id),
|
||||
queryKey: albumDiscsQueryKey,
|
||||
queryFn: () => fetchAlbumDiscs(api, album),
|
||||
})
|
||||
}
|
||||
|
||||
function warmArtistContext(api: Api | undefined, artistId: string): void {
|
||||
// Fail fast if we don't have an artist ID to work with
|
||||
if (!artistId) return
|
||||
|
||||
const queryKey = [QueryKeys.ArtistById, artistId]
|
||||
|
||||
// Bail out if we have data
|
||||
if (queryClient.getQueryState(queryKey)?.status === 'success') return
|
||||
|
||||
console.debug(`Warming context cache for artist ${artistId}`)
|
||||
/**
|
||||
* Store queryable of artist item
|
||||
*/
|
||||
queryClient.ensureQueryData({
|
||||
queryKey,
|
||||
queryFn: () => fetchItem(api, artistId!),
|
||||
})
|
||||
}
|
||||
|
||||
function warmTrackContext(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
track: BaseItemDto,
|
||||
streamingQuality: StreamingQuality,
|
||||
): void {
|
||||
const { Id, AlbumId, ArtistItems } = track
|
||||
|
||||
const mediaSourcesQueryKey = [QueryKeys.MediaSources, streamingQuality, Id]
|
||||
|
||||
if (queryClient.getQueryState(mediaSourcesQueryKey)?.status !== 'success')
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: mediaSourcesQueryKey,
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), Id!),
|
||||
})
|
||||
|
||||
const albumQueryKey = [QueryKeys.Album, AlbumId]
|
||||
|
||||
if (AlbumId && queryClient.getQueryState(albumQueryKey)?.status !== 'success')
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: albumQueryKey,
|
||||
queryFn: () => fetchItem(api, AlbumId!),
|
||||
})
|
||||
|
||||
if (ArtistItems) ArtistItems.forEach((artistItem) => warmArtistContext(api, artistItem.Id!))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user