diff --git a/src/components/Albums/component.tsx b/src/components/Albums/component.tsx index c1977c3d..00b69b86 100644 --- a/src/components/Albums/component.tsx +++ b/src/components/Albums/component.tsx @@ -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>() + const { api, user } = useJellifyContext() + + const streamingQuality = useStreamingQualityContext() + + const onViewableItemsChangedRef = useRef( + ({ viewableItems }: { viewableItems: ViewToken[] }) => { + 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={} stickyHeaderIndices={stickyHeaderIndices} removeClippedSubviews + onViewableItemsChanged={onViewableItemsChangedRef.current} /> ) diff --git a/src/components/Artist/albums.tsx b/src/components/Artist/albums.tsx index 1f4c8018..76a80bba 100644 --- a/src/components/Artist/albums.tsx +++ b/src/components/Artist/albums.tsx @@ -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[] }) => { + 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} /> ) } diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx index 87a1a9e0..7e17b561 100644 --- a/src/components/Artists/component.tsx +++ b/src/components/Artists/component.tsx @@ -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>() @@ -36,6 +44,15 @@ export default function Artists({ const pendingLetterRef = useRef(null) + const onViewableItemsChangedRef = useRef( + ({ viewableItems }: { viewableItems: ViewToken[] }) => { + 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 && ( diff --git a/src/components/Discover/component.tsx b/src/components/Discover/component.tsx index df3f1c77..e1b4fcc0 100644 --- a/src/components/Discover/component.tsx +++ b/src/components/Discover/component.tsx @@ -13,7 +13,7 @@ export default function Index(): React.JSX.Element { useDiscoverContext() return ( - + warmItemContext(api, user, props.item, streamingQuality)} {...props} > diff --git a/src/components/Global/components/item-row.tsx b/src/components/Global/components/item-row.tsx index 6dfeae59..9a3da47c 100644 --- a/src/components/Global/components/item-row.tsx +++ b/src/components/Global/components/item-row.tsx @@ -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, diff --git a/src/components/Playlists/component.tsx b/src/components/Playlists/component.tsx index 1d144705..c989a866 100644 --- a/src/components/Playlists/component.tsx +++ b/src/components/Playlists/component.tsx @@ -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>() + + const { api, user } = useJellifyContext() + + const streamingQuality = useStreamingQualityContext() + + const onViewableItemsChangedRef = useRef( + ({ viewableItems }: { viewableItems: ViewToken[] }) => { + viewableItems.forEach(({ isViewable, item }) => { + if (isViewable) warmItemContext(api, user, item, streamingQuality) + }) + }, + ) + return ( ) } diff --git a/src/components/Tracks/component.tsx b/src/components/Tracks/component.tsx index e4961c0e..e6bf115e 100644 --- a/src/components/Tracks/component.tsx +++ b/src/components/Tracks/component.tsx @@ -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[] }) => { + viewableItems.forEach(({ isViewable, item }) => { + if (isViewable) warmItemContext(api, user, item, streamingQuality) + }) + }, + ) + return ( 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!)) +}