mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-07 03:20:19 -06:00
Make Album Disc Fetching Lazy, User Data Query Caching Fixes (#504)
will use and store data retrieved from the server first, else will rely on a fetch this addresses battery life issues, as these queries were firing network requests far more frequently and for each item, causing unnecessary network usage and overhead update tanstack query to the latest while we're in there
This commit is contained in:
@@ -48,9 +48,9 @@
|
||||
"@sentry/react-native": "6.17.0",
|
||||
"@shopify/flash-list": "^2.0.3",
|
||||
"@tamagui/config": "^1.132.23",
|
||||
"@tanstack/query-async-storage-persister": "^5.85.6",
|
||||
"@tanstack/react-query": "^5.85.6",
|
||||
"@tanstack/react-query-persist-client": "^5.85.6",
|
||||
"@tanstack/query-async-storage-persister": "^5.85.9",
|
||||
"@tanstack/react-query": "^5.85.9",
|
||||
"@tanstack/react-query-persist-client": "^5.85.9",
|
||||
"@testing-library/react-native": "^13.2.3",
|
||||
"@typedigital/telemetrydeck-react": "^0.4.1",
|
||||
"axios": "^1.11.0",
|
||||
|
||||
@@ -32,7 +32,7 @@ export function fetchAlbums(
|
||||
parentId: library.musicLibraryId,
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
userId: user.id,
|
||||
enableUserData: false, // This data is fetched lazily on component render
|
||||
enableUserData: true, // This will populate the user data query later down the line
|
||||
sortBy,
|
||||
sortOrder,
|
||||
startIndex: page * ApiLimits.Library,
|
||||
|
||||
@@ -31,7 +31,7 @@ export function fetchArtists(
|
||||
.getAlbumArtists({
|
||||
parentId: library.musicLibraryId,
|
||||
userId: user.id,
|
||||
enableUserData: false, // This data is fetched lazily on component render
|
||||
enableUserData: true, // This will populate the User Data query later down the line
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
startIndex: page * ApiLimits.Library,
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
BaseItemKind,
|
||||
ItemSortBy,
|
||||
SortOrder,
|
||||
UserItemDataDto,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isUndefined } from 'lodash'
|
||||
@@ -178,33 +177,3 @@ export async function fetchFavoriteTracks(
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the {@link UserItemDataDto} for a given {@link BaseItemDto}
|
||||
* @param api The Jellyfin {@link Api} instance
|
||||
* @param itemId The ID field of the {@link BaseItemDto} to fetch user data for
|
||||
* @returns The {@link UserItemDataDto} for the given item
|
||||
*/
|
||||
export async function fetchUserData(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
itemId: string,
|
||||
): Promise<UserItemDataDto | void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
if (isUndefined(user)) return reject('User instance not set')
|
||||
|
||||
getItemsApi(api)
|
||||
.getItemUserData({
|
||||
itemId,
|
||||
userId: user.id,
|
||||
})
|
||||
.then((response) => {
|
||||
return resolve(response.data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export function fetchFrequentlyPlayed(
|
||||
startIndex: page * 100,
|
||||
sortBy: [ItemSortBy.PlayCount],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
enableUserData: true,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.Items) resolve(data.Items)
|
||||
|
||||
@@ -29,6 +29,7 @@ export async function fetchItem(api: Api | undefined, itemId: string): Promise<B
|
||||
.getItems({
|
||||
ids: [itemId],
|
||||
fields: [ItemFields.Tags],
|
||||
enableUserData: true,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items && response.data.TotalRecordCount == 1)
|
||||
|
||||
@@ -45,6 +45,7 @@ const useStreamedMediaInfo = (itemId: string | null | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: mediaInfoQueryKey({ api, user, deviceProfile, itemId }),
|
||||
queryFn: () => fetchMediaInfo(api, user, deviceProfile, itemId),
|
||||
staleTime: Infinity, // Only refetch when the user's device profile changes
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,5 +73,6 @@ export const useDownloadedMediaInfo = (itemId: string | null | undefined) => {
|
||||
return useQuery({
|
||||
queryKey: mediaInfoQueryKey({ api, user, deviceProfile, itemId }),
|
||||
queryFn: () => fetchMediaInfo(api, user, deviceProfile, itemId),
|
||||
staleTime: Infinity, // Only refetch when the user's device profile changes
|
||||
})
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export async function fetchRecentlyPlayed(
|
||||
sortBy: [ItemSortBy.DatePlayed],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
fields: [ItemFields.ParentId],
|
||||
enableUserData: true,
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug('Received recently played items response')
|
||||
|
||||
@@ -30,6 +30,7 @@ export function fetchTracks(
|
||||
.getItems({
|
||||
includeItemTypes: [BaseItemKind.Audio],
|
||||
parentId: library.musicLibraryId,
|
||||
enableUserData: true,
|
||||
userId: user.id,
|
||||
recursive: true,
|
||||
isFavorite: isFavorite,
|
||||
|
||||
17
src/api/queries/user-data/index.ts
Normal file
17
src/api/queries/user-data/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import fetchUserData from './utils'
|
||||
import UserDataQueryKey from './keys'
|
||||
import { ONE_MINUTE } from '@/src/constants/query-client'
|
||||
|
||||
export const useIsFavorite = (item: BaseItemDto) => {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
return useQuery({
|
||||
queryKey: UserDataQueryKey(user!, item),
|
||||
queryFn: () => fetchUserData(api, user, item.Id!),
|
||||
select: (data) => typeof data === 'object' && data.IsFavorite,
|
||||
enabled: !!api && !!user && !!item.Id, // Only run if we have the required data
|
||||
})
|
||||
}
|
||||
11
src/api/queries/user-data/keys.ts
Normal file
11
src/api/queries/user-data/keys.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { JellifyUser } from '../../../types/JellifyUser'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
|
||||
const UserDataQueryKey = (user: JellifyUser, item: BaseItemDto) => [
|
||||
QueryKeys.UserData,
|
||||
user.id,
|
||||
item.Id,
|
||||
]
|
||||
|
||||
export default UserDataQueryKey
|
||||
35
src/api/queries/user-data/utils/index.ts
Normal file
35
src/api/queries/user-data/utils/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { JellifyUser } from '@/src/types/JellifyUser'
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
import { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/user-item-data-dto'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
/**
|
||||
* Fetches the {@link UserItemDataDto} for a given {@link BaseItemDto}
|
||||
* @param api The Jellyfin {@link Api} instance
|
||||
* @param itemId The ID field of the {@link BaseItemDto} to fetch user data for
|
||||
* @returns The {@link UserItemDataDto} for the given item
|
||||
*/
|
||||
export default async function fetchUserData(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
itemId: string,
|
||||
): Promise<UserItemDataDto | void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
if (isUndefined(user)) return reject('User instance not set')
|
||||
|
||||
getItemsApi(api)
|
||||
.getItemUserData({
|
||||
itemId,
|
||||
userId: user.id,
|
||||
})
|
||||
.then((response) => {
|
||||
return resolve(response.data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -2,16 +2,13 @@ import { ActivityIndicator, RefreshControl } from 'react-native'
|
||||
import { getToken, Separator, XStack, YStack } from 'tamagui'
|
||||
import React, { RefObject, useEffect, useRef } from 'react'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list'
|
||||
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
||||
import { UseInfiniteQueryResult } 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 useStreamingDeviceProfile from '../../stores/device-profile'
|
||||
import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector'
|
||||
import { isString } from 'lodash'
|
||||
|
||||
@@ -28,23 +25,10 @@ export default function Albums({
|
||||
}: AlbumsProps): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const sectionListRef = useRef<FlashListRef<string | number | BaseItemDto>>(null)
|
||||
|
||||
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, deviceProfile)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
|
||||
const stickyHeaderIndices = React.useMemo(() => {
|
||||
if (!showAlphabeticalSelector || !albumsInfiniteQuery.data) return []
|
||||
@@ -155,7 +139,6 @@ export default function Albums({
|
||||
}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && albumPageParams && (
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, 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, ViewToken } from 'react-native'
|
||||
import { ActivityIndicator } 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 useStreamingDeviceProfile from '../../stores/device-profile'
|
||||
export default function Albums({
|
||||
route,
|
||||
navigation,
|
||||
}: ArtistAlbumsProps | ArtistEpsProps | ArtistFeaturedOnProps): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
const { albums, fetchingAlbums, featuredOn, scroll } = useArtistContext()
|
||||
const scrollHandler = useAnimatedScrollHandler({
|
||||
@@ -30,14 +22,6 @@ export default function Albums({
|
||||
},
|
||||
})
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable) warmItemContext(api, user, item, deviceProfile)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const [columns, setColumns] = useState(Math.floor(width / getToken('$20')))
|
||||
|
||||
useEffect(() => {
|
||||
@@ -113,7 +97,6 @@ export default function Albums({
|
||||
)
|
||||
}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,16 +5,13 @@ import { RefreshControl } from 'react-native'
|
||||
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, ViewToken } from '@shopify/flash-list'
|
||||
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
||||
import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector'
|
||||
import { UseInfiniteQueryResult, useMutation } from '@tanstack/react-query'
|
||||
import { UseInfiniteQueryResult } 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 useStreamingDeviceProfile from '../../stores/device-profile'
|
||||
|
||||
export interface ArtistsProps {
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<
|
||||
@@ -39,10 +36,6 @@ export default function Artists({
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const { isFavorites } = useLibrarySortAndFilterContext()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
@@ -52,15 +45,6 @@ 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, deviceProfile)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
||||
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
||||
|
||||
@@ -177,7 +161,6 @@ export default function Artists({
|
||||
}}
|
||||
// onEndReachedThreshold default is 0.5
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && artistPageParams && (
|
||||
|
||||
@@ -1,44 +1,22 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import React from 'react'
|
||||
import Icon from './icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { Spinner } from 'tamagui'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/favorites'
|
||||
import { useJellifyUserDataContext } from '../../../providers/UserData'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { ONE_HOUR } from '../../../constants/query-client'
|
||||
import { useIsFavorite } from '../../../api/queries/user-data'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto
|
||||
}
|
||||
|
||||
export default function FavoriteButton({
|
||||
item,
|
||||
onToggle,
|
||||
}: {
|
||||
interface FavoriteButtonProps {
|
||||
item: BaseItemDto
|
||||
onToggle?: () => void
|
||||
}): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
}
|
||||
|
||||
export default function FavoriteButton({ item, onToggle }: FavoriteButtonProps): React.JSX.Element {
|
||||
const { toggleFavorite } = useJellifyUserDataContext()
|
||||
|
||||
const {
|
||||
data: isFavorite,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id],
|
||||
queryFn: () => fetchUserData(api, user, item.Id!),
|
||||
select: (data) => typeof data === 'object' && data.IsFavorite,
|
||||
staleTime: ONE_HOUR,
|
||||
})
|
||||
const { data: isFavorite } = useIsFavorite(item)
|
||||
|
||||
return isFetching ? (
|
||||
<Spinner alignSelf='center' />
|
||||
) : isFavorite ? (
|
||||
return isFavorite ? (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
<Icon
|
||||
name={'heart'}
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/favorites'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { ListItem, XStack } from 'tamagui'
|
||||
import Icon from './icon'
|
||||
import { useJellifyUserDataContext } from '../../../providers/UserData'
|
||||
import { Text } from '../helpers/text'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { ONE_HOUR } from '../../../constants/query-client'
|
||||
import { useIsFavorite } from '../../../api/queries/user-data'
|
||||
|
||||
export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
const { toggleFavorite } = useJellifyUserDataContext()
|
||||
|
||||
const { data: isFavorite, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id],
|
||||
queryFn: () => fetchUserData(api, user, item.Id!),
|
||||
select: (data) => typeof data === 'object' && data.IsFavorite,
|
||||
staleTime: ONE_HOUR,
|
||||
})
|
||||
const { data: isFavorite, refetch } = useIsFavorite(item)
|
||||
|
||||
return isFavorite ? (
|
||||
<ListItem
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Spacer } from 'tamagui'
|
||||
import Icon from './icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/favorites'
|
||||
import { memo } from 'react'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { useIsFavorite } from '../../../api/queries/user-data'
|
||||
|
||||
/**
|
||||
* This component is used to display a favorite icon for a given item.
|
||||
@@ -16,14 +13,7 @@ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
* @returns A React component that displays a favorite icon for a given item.
|
||||
*/
|
||||
function FavoriteIcon({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const { data: isFavorite } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id],
|
||||
queryFn: () => fetchUserData(api, user, item.Id!),
|
||||
select: (data) => typeof data === 'object' && data.IsFavorite,
|
||||
enabled: !!api && !!user && !!item.Id, // Only run if we have the required data
|
||||
})
|
||||
const { data: isFavorite } = useIsFavorite(item)
|
||||
|
||||
return isFavorite ? (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import useStreamingDeviceProfile from '../../../stores/device-profile'
|
||||
import { warmItemContext } from '../../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
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'
|
||||
import { FlashList, FlashListProps } from '@shopify/flash-list'
|
||||
import React from 'react'
|
||||
|
||||
interface HorizontalCardListProps extends FlashListProps<BaseItemDto> {}
|
||||
|
||||
@@ -16,23 +13,10 @@ interface HorizontalCardListProps extends FlashListProps<BaseItemDto> {}
|
||||
export default function HorizontalCardList({
|
||||
...props
|
||||
}: HorizontalCardListProps): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
|
||||
viewableItems
|
||||
.filter(({ isViewable }) => isViewable)
|
||||
.forEach(({ item }) => warmItemContext(api, user, item, deviceProfile))
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
horizontal
|
||||
data={props.data}
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
renderItem={props.renderItem}
|
||||
removeClippedSubviews
|
||||
style={{
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 useItemContext from '../../../hooks/use-item-context'
|
||||
|
||||
interface CardProps extends TamaguiCardProps {
|
||||
caption?: string | null | undefined
|
||||
@@ -21,6 +22,8 @@ interface CardProps extends TamaguiCardProps {
|
||||
* @param props
|
||||
*/
|
||||
export function ItemCard(props: CardProps) {
|
||||
const warmContext = useItemContext(props.item)
|
||||
|
||||
return (
|
||||
<View alignItems='center' margin={'$1.5'}>
|
||||
<TamaguiCard
|
||||
@@ -32,6 +35,7 @@ export function ItemCard(props: CardProps) {
|
||||
circular={!props.squared}
|
||||
borderRadius={props.squared ? '$5' : 'unset'}
|
||||
animation='bouncy'
|
||||
onPressIn={warmContext}
|
||||
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
|
||||
pressStyle={props.onPress ? { scale: 0.875 } : {}}
|
||||
{...props}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { useNetworkStatus } from '../../../stores/network'
|
||||
import useStreamingDeviceProfile from '../../../stores/device-profile'
|
||||
import useItemContext from '../../../hooks/use-item-context'
|
||||
|
||||
interface ItemRowProps {
|
||||
item: BaseItemDto
|
||||
@@ -49,6 +50,8 @@ export default function ItemRow({
|
||||
|
||||
const { mutate: loadNewQueue } = useLoadNewQueue()
|
||||
|
||||
const warmContext = useItemContext(item)
|
||||
|
||||
const gestureCallback = () => {
|
||||
switch (item.Type) {
|
||||
case 'Audio': {
|
||||
@@ -82,6 +85,7 @@ export default function ItemRow({
|
||||
alignContent='center'
|
||||
minHeight={'$7'}
|
||||
width={'100%'}
|
||||
onPressIn={warmContext}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', {
|
||||
item,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useCallback } from 'react'
|
||||
import React, { useMemo, useCallback, useEffect } from 'react'
|
||||
import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
@@ -69,7 +69,7 @@ export default function Track({
|
||||
|
||||
const offlineAudio = useDownloadedTrack(track.Id)
|
||||
|
||||
useItemContext(track)
|
||||
const warmContext = useItemContext(track)
|
||||
|
||||
// Memoize expensive computations
|
||||
const isPlaying = useMemo(
|
||||
@@ -159,6 +159,10 @@ export default function Track({
|
||||
[showArtwork, track.Artists],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
warmContext()
|
||||
}, [track])
|
||||
|
||||
return (
|
||||
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
|
||||
<XStack
|
||||
@@ -167,6 +171,7 @@ export default function Track({
|
||||
height={showArtwork ? '$6' : '$5'}
|
||||
flex={1}
|
||||
testID={testID ?? undefined}
|
||||
onPressIn={warmContext}
|
||||
onPress={handlePress}
|
||||
onLongPress={handleLongPress}
|
||||
paddingVertical={'$2'}
|
||||
|
||||
@@ -58,10 +58,10 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
>
|
||||
<Icon
|
||||
name={isFavorites ? 'heart' : 'heart-outline'}
|
||||
color={isFavorites ? '$secondary' : '$borderColor'}
|
||||
color={isFavorites ? '$primary' : '$borderColor'}
|
||||
/>
|
||||
|
||||
<Text color={isFavorites ? '$secondary' : '$borderColor'}>
|
||||
<Text color={isFavorites ? '$primary' : '$borderColor'}>
|
||||
{isFavorites ? 'Favorites' : 'All'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { RefreshControl } from 'react-native-gesture-handler'
|
||||
import { Separator } from 'tamagui'
|
||||
import { FlashList, ViewToken } from '@shopify/flash-list'
|
||||
import { FlashList } 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 useStreamingDeviceProfile from '../../stores/device-profile'
|
||||
|
||||
export interface PlaylistsProps {
|
||||
canEdit?: boolean | undefined
|
||||
@@ -32,18 +28,6 @@ export default function Playlists({
|
||||
}: PlaylistsProps): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable) warmItemContext(api, user, item, deviceProfile)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
@@ -68,7 +52,6 @@ export default function Playlists({
|
||||
}
|
||||
}}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React, { useRef } from 'react'
|
||||
import React from 'react'
|
||||
import Track from '../Global/components/track'
|
||||
import { getTokens, Separator } from 'tamagui'
|
||||
import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { FlashList, ViewToken } from '@shopify/flash-list'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '../../screens/types'
|
||||
import { warmItemContext } from '../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import useStreamingDeviceProfile from '../../stores/device-profile'
|
||||
import { useAllDownloadedTracks } from '../../api/queries/download'
|
||||
import UserDataQueryKey from '../../api/queries/user-data/keys'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
|
||||
interface TracksProps {
|
||||
tracks: (string | number | BaseItemDto)[] | undefined
|
||||
@@ -32,9 +30,8 @@ export default function Tracks({
|
||||
filterDownloaded,
|
||||
filterFavorites,
|
||||
}: TracksProps): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
const { user } = useJellifyContext()
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
const { data: downloadedTracks } = useAllDownloadedTracks()
|
||||
|
||||
// Memoize the expensive tracks processing to prevent memory leaks
|
||||
@@ -47,10 +44,9 @@ export default function Tracks({
|
||||
if (filterFavorites) {
|
||||
return (
|
||||
(
|
||||
queryClient.getQueryData([
|
||||
QueryKeys.UserData,
|
||||
downloadedTrack.Id,
|
||||
]) as UserItemDataDto | undefined
|
||||
queryClient.getQueryData(
|
||||
UserDataQueryKey(user!, downloadedTrack),
|
||||
) as UserItemDataDto | undefined
|
||||
)?.IsFavorite ?? false
|
||||
)
|
||||
}
|
||||
@@ -79,14 +75,6 @@ export default function Tracks({
|
||||
[tracksToDisplay, queue],
|
||||
)
|
||||
|
||||
const onViewableItemsChangedRef = useRef(
|
||||
({ viewableItems }: { viewableItems: ViewToken<BaseItemDto>[] }) => {
|
||||
viewableItems.forEach(({ isViewable, item }) => {
|
||||
if (isViewable) warmItemContext(api, user, item, deviceProfile)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
|
||||
@@ -6,12 +6,13 @@ import { QueryKeys } from '../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../api/queries/media/utils'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../api/queries/item'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { fetchUserData } from '../api/queries/favorites'
|
||||
import fetchUserData from '../api/queries/user-data/utils'
|
||||
import { useJellifyContext } from '../providers'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../stores/device-profile'
|
||||
import UserDataQueryKey from '../api/queries/user-data/keys'
|
||||
|
||||
export default function useItemContext(item: BaseItemDto): void {
|
||||
export default function useItemContext(item: BaseItemDto): () => void {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingDeviceProfile = useStreamingDeviceProfile()
|
||||
@@ -20,7 +21,7 @@ export default function useItemContext(item: BaseItemDto): void {
|
||||
|
||||
const prefetchedContext = useRef<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
return useCallback(() => {
|
||||
const effectSig = `${item.Id}-${item.Type}`
|
||||
|
||||
// If we've already warmed the cache for this item, return
|
||||
@@ -33,7 +34,7 @@ export default function useItemContext(item: BaseItemDto): void {
|
||||
}, [api, user, streamingDeviceProfile])
|
||||
}
|
||||
|
||||
export function warmItemContext(
|
||||
function warmItemContext(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
item: BaseItemDto,
|
||||
@@ -72,12 +73,11 @@ export function warmItemContext(
|
||||
}),
|
||||
})
|
||||
|
||||
const userDataQueryKey = [QueryKeys.UserData, Id]
|
||||
if (queryClient.getQueryState(userDataQueryKey)?.status !== 'success') {
|
||||
if (UserData) queryClient.setQueryData([QueryKeys.UserData, Id], UserData)
|
||||
if (queryClient.getQueryState(UserDataQueryKey(user!, item))?.status !== 'success') {
|
||||
if (UserData) queryClient.setQueryData(UserDataQueryKey(user!, item), UserData)
|
||||
else
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [],
|
||||
queryKey: UserDataQueryKey(user!, item),
|
||||
queryFn: () => fetchUserData(api, user, Id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { QueryKeys } from '../../enums/query-keys'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useJellifyContext } from '..'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
import UserDataQueryKey from '../../api/queries/user-data/keys'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto
|
||||
@@ -19,7 +20,7 @@ interface JellifyUserDataContext {
|
||||
}
|
||||
|
||||
const JellifyUserDataContextInitializer = () => {
|
||||
const { api } = useJellifyContext()
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
@@ -45,7 +46,7 @@ const JellifyUserDataContextInitializer = () => {
|
||||
if (onToggle) onToggle()
|
||||
|
||||
// Force refresh of track user data
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKeys.UserData, item.Id] })
|
||||
queryClient.invalidateQueries({ queryKey: UserDataQueryKey(user!, item) })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -70,7 +71,7 @@ const JellifyUserDataContextInitializer = () => {
|
||||
if (onToggle) onToggle()
|
||||
|
||||
// Force refresh of track user data
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKeys.UserData, item.Id] })
|
||||
queryClient.invalidateQueries({ queryKey: UserDataQueryKey(user!, item) })
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
48
yarn.lock
48
yarn.lock
@@ -3402,38 +3402,38 @@
|
||||
resolved "https://registry.yarnpkg.com/@tamagui/z-index-stack/-/z-index-stack-1.132.23.tgz#a74f06f3b6a6191951f396105f39a10aec0144aa"
|
||||
integrity sha512-djbRW7FWzuc9bCIVXG00pVa6McM8/H8R4JOL+szxSy1iAo0P2k0OzWfBb+ZbbjTye068fBPGIniq4X7+3huS1Q==
|
||||
|
||||
"@tanstack/query-async-storage-persister@^5.85.6":
|
||||
version "5.85.6"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.85.6.tgz#a215d1a78fe23efab45b6f31d942cfe4515c5c36"
|
||||
integrity sha512-f2C0tMVEo6oFdcNE1xOYGJ5KB02cIEDPjbWuqEivJfrlWDqsPlnHnfxTYkuMcddTHTClDf/sqtjuB95zggeEsQ==
|
||||
"@tanstack/query-async-storage-persister@^5.85.9":
|
||||
version "5.85.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.85.9.tgz#9f80f4f612a35b67cb4bee4238f1c24adb29515d"
|
||||
integrity sha512-6+HevrTPQ8TbJ3iMPY9NckQ4eLTIHgoZZflQVIKKcpb7Y08OQ15kw8skm7CCkTZClsTz1+JNr9heuMCBluO+rw==
|
||||
dependencies:
|
||||
"@tanstack/query-persist-client-core" "5.85.6"
|
||||
"@tanstack/query-persist-client-core" "5.85.9"
|
||||
|
||||
"@tanstack/query-core@5.85.6":
|
||||
version "5.85.6"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.85.6.tgz#2af90f3c56c38fd2194e0ed1996122373c4bfca5"
|
||||
integrity sha512-hCj0TktzdCv2bCepIdfwqVwUVWb+GSHm1Jnn8w+40lfhQ3m7lCO7ADRUJy+2unxQ/nzjh2ipC6ye69NDW3l73g==
|
||||
"@tanstack/query-core@5.85.9":
|
||||
version "5.85.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.85.9.tgz#69e800f4d1c42ca4a110469229d8ecca409d477a"
|
||||
integrity sha512-5fxb9vwyftYE6KFLhhhDyLr8NO75+Wpu7pmTo+TkwKmMX2oxZDoLwcqGP8ItKSpUMwk3urWgQDZfyWr5Jm9LsQ==
|
||||
|
||||
"@tanstack/query-persist-client-core@5.85.6":
|
||||
version "5.85.6"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.6.tgz#92f9d3ac83f51b0bd452fe3b65047f4196c3f56a"
|
||||
integrity sha512-wUdoEurIC0YCNZzR020Xcg3OsJeF4SXmEPqlNwZ6EaGKgWeNjU17hVdK+X4ZeirUm+h0muiEQx+aIQU1lk7roQ==
|
||||
"@tanstack/query-persist-client-core@5.85.9":
|
||||
version "5.85.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.9.tgz#2d7e89d37195a307d898d850652c1f4fca77158d"
|
||||
integrity sha512-er7HfMjn6TQanWG5nudjRNZbo7ahf7IIdWN5kU7L2qRZ2kcw89TQZAZ74GIQsumOXZD7sUcHG2dylveFZNxlZA==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.85.6"
|
||||
"@tanstack/query-core" "5.85.9"
|
||||
|
||||
"@tanstack/react-query-persist-client@^5.85.6":
|
||||
version "5.85.6"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.85.6.tgz#40c59526f55dc4ee5ac8ddb5bc777e27e2256627"
|
||||
integrity sha512-zLUfm8JlI6/s0AqvX5l5CcazdHwj5gwcv0mWYOaJJvADyFzl2wwQKqB/H4nYSeygUtrepBgPwVQKNqH9ZwlZpQ==
|
||||
"@tanstack/react-query-persist-client@^5.85.9":
|
||||
version "5.85.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.85.9.tgz#5e01d5d0744098fa7d0feefc397068092fcdfcb5"
|
||||
integrity sha512-h75Xyt/XDtk1oRmli5znQRXclT/iZpseTK7ScqEjaiZmPSREgg2mJb1blo2SB1swSidDNsCtnENLNH43PP0/9w==
|
||||
dependencies:
|
||||
"@tanstack/query-persist-client-core" "5.85.6"
|
||||
"@tanstack/query-persist-client-core" "5.85.9"
|
||||
|
||||
"@tanstack/react-query@^5.85.6":
|
||||
version "5.85.6"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.85.6.tgz#0885cd9e02f8a5aa228f6b5dc2122d22ba597d68"
|
||||
integrity sha512-VUAag4ERjh+qlmg0wNivQIVCZUrYndqYu3/wPCVZd4r0E+1IqotbeyGTc+ICroL/PqbpSaGZg02zSWYfcvxbdA==
|
||||
"@tanstack/react-query@^5.85.9":
|
||||
version "5.85.9"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.85.9.tgz#d27315b3f327af31237d513e505f3e6a2cad5739"
|
||||
integrity sha512-2T5zgSpcOZXGkH/UObIbIkGmUPQqZqn7esVQFXLOze622h4spgWf5jmvrqAo9dnI13/hyMcNsF1jsoDcb59nJQ==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.85.6"
|
||||
"@tanstack/query-core" "5.85.9"
|
||||
|
||||
"@telemetrydeck/sdk@^2.0.4":
|
||||
version "2.0.4"
|
||||
|
||||
Reference in New Issue
Block a user