mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-07 19:40:19 -06:00
Alphabet Selector for Albums, Dep updates (#501)
Get alphabetical selector working for albums. Now an A-Z will appear to the right of the albums list and users can use that to get to a certain letter of albums in the list Under the hood - also updates the dependencies for React Native and Tanstack Query
This commit is contained in:
606
ios/Podfile.lock
606
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -48,10 +48,10 @@
|
||||
"@sentry/react-native": "6.17.0",
|
||||
"@shopify/flash-list": "^2.0.3",
|
||||
"@tamagui/config": "^1.132.23",
|
||||
"@tanstack/query-async-storage-persister": "^5.85.5",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"@tanstack/react-query-persist-client": "^5.85.5",
|
||||
"@testing-library/react-native": "^13.2.2",
|
||||
"@tanstack/query-async-storage-persister": "^5.85.6",
|
||||
"@tanstack/react-query": "^5.85.6",
|
||||
"@tanstack/react-query-persist-client": "^5.85.6",
|
||||
"@testing-library/react-native": "^13.2.3",
|
||||
"@typedigital/telemetrydeck-react": "^0.4.1",
|
||||
"axios": "^1.11.0",
|
||||
"bundle": "^2.1.0",
|
||||
@@ -59,10 +59,10 @@
|
||||
"gem": "^2.4.3",
|
||||
"invert-color": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"openai": "^5.12.2",
|
||||
"openai": "^5.16.0",
|
||||
"react": "19.1.0",
|
||||
"react-freeze": "^1.0.4",
|
||||
"react-native": "0.81.0",
|
||||
"react-native": "0.81.1",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-blob-util": "^0.22.2",
|
||||
"react-native-blurhash": "2.1.1",
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
ItemSortBy,
|
||||
SortOrder,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { JellifyLibrary } from '../../types/JellifyLibrary'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { fetchItem, fetchItems } from './item'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
export function fetchAlbums(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
page: string,
|
||||
isFavorite: boolean | undefined,
|
||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
): Promise<{ title: string | number; data: BaseItemDto[] }> {
|
||||
console.debug('Fetching albums', page)
|
||||
|
||||
return fetchItems(
|
||||
api,
|
||||
user,
|
||||
library,
|
||||
[BaseItemKind.MusicAlbum],
|
||||
page,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
isFavorite,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAlbumById(api: Api | undefined, albumId: string): Promise<BaseItemDto> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchItem(api, albumId)
|
||||
.then((item) => {
|
||||
resolve(item)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
79
src/api/queries/album/index.ts
Normal file
79
src/api/queries/album/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'
|
||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'
|
||||
import { fetchAlbums } from './utils/album'
|
||||
import { RefObject, useCallback, useRef } from 'react'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||
import { ApiLimits } from '../query.config'
|
||||
import { fetchRecentlyAdded } from '../recents'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
|
||||
const useAlbums: () => [
|
||||
RefObject<Set<string>>,
|
||||
UseInfiniteQueryResult<(string | number | BaseItemDto)[]>,
|
||||
] = () => {
|
||||
const { api, user, library } = useJellifyContext()
|
||||
|
||||
const { isFavorites, sortDescending } = useLibrarySortAndFilterContext()
|
||||
|
||||
const albumPageParams = useRef<Set<string>>(new Set<string>())
|
||||
|
||||
// Memize the expensive albums select function
|
||||
const selectAlbums = useCallback(
|
||||
(data: InfiniteData<BaseItemDto[], unknown>) =>
|
||||
flattenInfiniteQueryPages(data, albumPageParams),
|
||||
[],
|
||||
)
|
||||
|
||||
const albumsInfiniteQuery = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.InfiniteAlbums, isFavorites, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchAlbums(
|
||||
api,
|
||||
user,
|
||||
library,
|
||||
pageParam,
|
||||
isFavorites,
|
||||
[ItemSortBy.SortName],
|
||||
[SortOrder.Ascending],
|
||||
),
|
||||
initialPageParam: 0,
|
||||
select: selectAlbums,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
||||
},
|
||||
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => {
|
||||
return firstPageParam === 0 ? null : firstPageParam - 1
|
||||
},
|
||||
})
|
||||
|
||||
return [albumPageParams, albumsInfiniteQuery]
|
||||
}
|
||||
|
||||
export default useAlbums
|
||||
|
||||
export const useRecentlyAddedAlbums = () => {
|
||||
const { api, user, library } = useJellifyContext()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRefetchRecentlyAdded: () => () => void = () => {
|
||||
const { library } = useJellifyContext()
|
||||
|
||||
return () =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId],
|
||||
})
|
||||
}
|
||||
61
src/api/queries/album/utils/album.ts
Normal file
61
src/api/queries/album/utils/album.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
ItemFields,
|
||||
ItemSortBy,
|
||||
SortOrder,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { fetchItem, fetchItems } from '../../item'
|
||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { ApiLimits } from '../../query.config'
|
||||
export function fetchAlbums(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
page: number,
|
||||
isFavorite: boolean | undefined,
|
||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
): Promise<BaseItemDto[]> {
|
||||
console.debug('Fetching albums', page)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!api) return reject('No API instance provided')
|
||||
if (!user) return reject('No user provided')
|
||||
if (!library) return reject('Library has not been set')
|
||||
|
||||
getItemsApi(api)
|
||||
.getItems({
|
||||
parentId: library.musicLibraryId,
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
userId: user.id,
|
||||
enableUserData: false, // This data is fetched lazily on component render
|
||||
sortBy,
|
||||
sortOrder,
|
||||
startIndex: page * ApiLimits.Library,
|
||||
limit: ApiLimits.Library,
|
||||
isFavorite,
|
||||
fields: [ItemFields.SortName],
|
||||
recursive: true,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
console.debug('Albums Response receieved')
|
||||
return data.Items ? resolve(data.Items) : resolve([])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchAlbumById(api: Api | undefined, albumId: string): Promise<BaseItemDto> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchItem(api, albumId)
|
||||
.then((item) => {
|
||||
resolve(item)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,9 @@ import { isString, isUndefined } from 'lodash'
|
||||
import { fetchArtistAlbums, fetchArtistFeaturedOn, fetchArtists } from './utils/artist'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { ApiLimits } from '../query.config'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { RefObject, useCallback, useRef } from 'react'
|
||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
||||
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||
|
||||
export const useArtistAlbums = (artist: BaseItemDto) => {
|
||||
const { api, library } = useJellifyContext()
|
||||
@@ -32,60 +34,22 @@ export const useArtistFeaturedOn = (artist: BaseItemDto) => {
|
||||
})
|
||||
}
|
||||
|
||||
interface AlbumArtistQueryParams {
|
||||
isFavorites: boolean | undefined
|
||||
sortDescending: boolean
|
||||
}
|
||||
|
||||
export const useAlbumArtists: (
|
||||
params: AlbumArtistQueryParams,
|
||||
) => [
|
||||
React.RefObject<Set<string>>,
|
||||
export const useAlbumArtists: () => [
|
||||
RefObject<Set<string>>,
|
||||
UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>,
|
||||
] = ({ isFavorites, sortDescending }: AlbumArtistQueryParams) => {
|
||||
] = () => {
|
||||
const { api, user, library } = useJellifyContext()
|
||||
|
||||
const { isFavorites, sortDescending } = useLibrarySortAndFilterContext()
|
||||
|
||||
const artistPageParams = useRef<Set<string>>(new Set<string>())
|
||||
|
||||
// Memoize the expensive artists select function
|
||||
const selectArtists = useCallback((data: InfiniteData<BaseItemDto[], unknown>) => {
|
||||
/**
|
||||
* A flattened array of all artists derived from the infinite query
|
||||
*/
|
||||
const flattenedArtistPages = data.pages.flatMap((page) => page)
|
||||
|
||||
/**
|
||||
* A set of letters we've seen so we can add them to the alphabetical selector
|
||||
*/
|
||||
const seenLetters = new Set<string>()
|
||||
|
||||
/**
|
||||
* The final array that will be provided to and rendered by the {@link Artists} component
|
||||
*/
|
||||
const flashArtistList: (string | number | BaseItemDto)[] = []
|
||||
|
||||
flattenedArtistPages.forEach((artist: BaseItemDto) => {
|
||||
const rawLetter = isString(artist.SortName)
|
||||
? artist.SortName.trim().charAt(0).toUpperCase()
|
||||
: '#'
|
||||
|
||||
/**
|
||||
* An alpha character or a hash if the artist's name doesn't start with a letter
|
||||
*/
|
||||
const letter = rawLetter.match(/[A-Z]/) ? rawLetter : '#'
|
||||
|
||||
if (!seenLetters.has(letter)) {
|
||||
seenLetters.add(letter)
|
||||
flashArtistList.push(letter)
|
||||
}
|
||||
|
||||
flashArtistList.push(artist)
|
||||
})
|
||||
|
||||
artistPageParams.current = seenLetters
|
||||
|
||||
return flashArtistList
|
||||
}, [])
|
||||
const selectArtists = useCallback(
|
||||
(data: InfiniteData<BaseItemDto[], unknown>) =>
|
||||
flattenInfiniteQueryPages(data, artistPageParams),
|
||||
[],
|
||||
)
|
||||
|
||||
const artistsInfiniteQuery = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.InfiniteArtists, isFavorites, sortDescending, library?.musicLibraryId],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageFormat } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export enum ApiLimits {
|
||||
Library = 100,
|
||||
Library = 200,
|
||||
}
|
||||
|
||||
const QueryConfig = {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ActivityIndicator, RefreshControl } from 'react-native'
|
||||
import { getToken, Separator, XStack, YStack } from 'tamagui'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { RefObject, useEffect, useRef } from 'react'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { FlashList, ViewToken } from '@shopify/flash-list'
|
||||
import { FetchNextPageOptions } from '@tanstack/react-query'
|
||||
import { FlashList, FlashListRef, ViewToken } 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'
|
||||
@@ -12,21 +12,18 @@ 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'
|
||||
|
||||
interface AlbumsProps {
|
||||
albums: (string | number | BaseItemDto)[] | undefined
|
||||
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
isFetchingNextPage: boolean
|
||||
albumsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
showAlphabeticalSelector: boolean
|
||||
albumPageParams?: RefObject<Set<string>>
|
||||
}
|
||||
|
||||
export default function Albums({
|
||||
albums,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isPending,
|
||||
albumsInfiniteQuery,
|
||||
albumPageParams,
|
||||
showAlphabeticalSelector,
|
||||
}: AlbumsProps): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
@@ -35,6 +32,10 @@ export default function Albums({
|
||||
|
||||
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 }) => {
|
||||
@@ -46,21 +47,63 @@ export default function Albums({
|
||||
|
||||
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
|
||||
const stickyHeaderIndices = React.useMemo(() => {
|
||||
if (!showAlphabeticalSelector || !albums) return []
|
||||
if (!showAlphabeticalSelector || !albumsInfiniteQuery.data) return []
|
||||
|
||||
return albums
|
||||
return albumsInfiniteQuery.data
|
||||
.map((album, index) => (typeof album === 'string' ? index : 0))
|
||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||
}, [showAlphabeticalSelector, albums])
|
||||
}, [showAlphabeticalSelector, albumsInfiniteQuery.data])
|
||||
|
||||
const { mutate: alphabetSelectorMutate } = useAlphabetSelector(
|
||||
(letter) => (pendingLetterRef.current = letter.toUpperCase()),
|
||||
)
|
||||
|
||||
// Effect for handling the pending alphabet selector letter
|
||||
useEffect(() => {
|
||||
if (isString(pendingLetterRef.current) && albumsInfiniteQuery.data) {
|
||||
const upperLetters = albumsInfiniteQuery.data
|
||||
.filter((item): item is string => typeof item === 'string')
|
||||
.map((letter) => letter.toUpperCase())
|
||||
.sort()
|
||||
|
||||
const index = upperLetters.findIndex((letter) => letter >= pendingLetterRef.current!)
|
||||
|
||||
if (index !== -1) {
|
||||
const letterToScroll = upperLetters[index]
|
||||
const scrollIndex = albumsInfiniteQuery.data.indexOf(letterToScroll)
|
||||
if (scrollIndex !== -1) {
|
||||
sectionListRef.current?.scrollToIndex({
|
||||
index: scrollIndex,
|
||||
viewPosition: 0.1,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// fallback: scroll to last section
|
||||
const lastLetter = upperLetters[upperLetters.length - 1]
|
||||
const scrollIndex = albumsInfiniteQuery.data.indexOf(lastLetter)
|
||||
if (scrollIndex !== -1) {
|
||||
sectionListRef.current?.scrollToIndex({
|
||||
index: scrollIndex,
|
||||
viewPosition: 0.1,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pendingLetterRef.current = null
|
||||
}
|
||||
}, [pendingLetterRef.current, albumsInfiniteQuery.data])
|
||||
|
||||
return (
|
||||
<XStack flex={1}>
|
||||
<FlashList
|
||||
ref={sectionListRef}
|
||||
contentContainerStyle={{
|
||||
paddingTop: getToken('$1'),
|
||||
}}
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
data={albums ?? []}
|
||||
data={albumsInfiniteQuery.data ?? []}
|
||||
keyExtractor={(item) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
@@ -89,7 +132,7 @@ export default function Albums({
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
isPending ? (
|
||||
albumsInfiniteQuery.isPending ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<YStack justifyContent='center'>
|
||||
@@ -98,15 +141,34 @@ export default function Albums({
|
||||
)
|
||||
}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage) fetchNextPage()
|
||||
if (albumsInfiniteQuery.hasNextPage) albumsInfiniteQuery.fetchNextPage()
|
||||
}}
|
||||
ListFooterComponent={isPending ? <ActivityIndicator /> : null}
|
||||
ListFooterComponent={
|
||||
albumsInfiniteQuery.isFetchingNextPage ? <ActivityIndicator /> : null
|
||||
}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
refreshControl={<RefreshControl refreshing={isPending} />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={albumsInfiniteQuery.isFetching}
|
||||
onRefresh={albumsInfiniteQuery.refetch}
|
||||
/>
|
||||
}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
removeClippedSubviews
|
||||
onViewableItemsChanged={onViewableItemsChangedRef.current}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && albumPageParams && (
|
||||
<AZScroller
|
||||
onLetterSelect={(letter) =>
|
||||
alphabetSelectorMutate({
|
||||
letter,
|
||||
infiniteQuery: albumsInfiniteQuery,
|
||||
pageParams: albumPageParams,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { RefObject, useEffect, useMemo, useRef } from 'react'
|
||||
import { getToken, Separator, useTheme, XStack } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { RefreshControl } from 'react-native'
|
||||
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, ViewToken } from '@shopify/flash-list'
|
||||
import { AZScroller } from '../Global/components/alphabetical-selector'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector'
|
||||
import { UseInfiniteQueryResult, useMutation } from '@tanstack/react-query'
|
||||
import { isString } from 'lodash'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
@@ -17,6 +16,15 @@ import { warmItemContext } from '../../hooks/use-item-context'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import useStreamingDeviceProfile from '../../stores/device-profile'
|
||||
|
||||
export interface ArtistsProps {
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<
|
||||
BaseItemDto[] | (string | number | BaseItemDto)[],
|
||||
Error
|
||||
>
|
||||
showAlphabeticalSelector: boolean
|
||||
artistPageParams?: RefObject<Set<string>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @param artistsInfiniteQuery - The infinite query for artists
|
||||
* @param navigation - The navigation object
|
||||
@@ -53,30 +61,20 @@ export default function Artists({
|
||||
},
|
||||
)
|
||||
|
||||
const alphabeticalSelectorCallback = async (letter: string) => {
|
||||
console.debug(`Alphabetical Selector Callback: ${letter}`)
|
||||
const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
||||
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
||||
|
||||
while (
|
||||
!artistPageParams!.current.has(letter.toUpperCase()) &&
|
||||
artistsInfiniteQuery.hasNextPage
|
||||
) {
|
||||
if (!artistsInfiniteQuery.isPending) {
|
||||
await artistsInfiniteQuery.fetchNextPage()
|
||||
}
|
||||
}
|
||||
console.debug(`Alphabetical Selector Callback: ${letter} complete`)
|
||||
}
|
||||
const stickyHeaderIndices = useMemo(() => {
|
||||
if (!showAlphabeticalSelector || !artists) return []
|
||||
|
||||
const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } = useMutation({
|
||||
mutationFn: (letter: string) => alphabeticalSelectorCallback(letter),
|
||||
onSuccess: (data: void, letter: string) => {
|
||||
pendingLetterRef.current = letter.toUpperCase()
|
||||
},
|
||||
})
|
||||
return artists
|
||||
.map((artist, index, artists) => (typeof artist === 'string' ? index : 0))
|
||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||
}, [showAlphabeticalSelector, artists])
|
||||
|
||||
// Effect for handling the pending alphabet selector letter
|
||||
useEffect(() => {
|
||||
if (isString(pendingLetterRef.current) && artistsInfiniteQuery.data) {
|
||||
if (isString(pendingLetterRef.current) && artists) {
|
||||
const upperLetters = artists
|
||||
.filter((item): item is string => typeof item === 'string')
|
||||
.map((letter) => letter.toUpperCase())
|
||||
@@ -168,15 +166,7 @@ export default function Artists({
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
stickyHeaderIndices={
|
||||
showAlphabeticalSelector
|
||||
? artists
|
||||
?.map((artist, index, artists) =>
|
||||
typeof artist === 'string' ? index : 0,
|
||||
)
|
||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||
: []
|
||||
}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
onStartReached={() => {
|
||||
if (artistsInfiniteQuery.hasPreviousPage)
|
||||
artistsInfiniteQuery.fetchPreviousPage()
|
||||
@@ -191,7 +181,15 @@ export default function Artists({
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && artistPageParams && (
|
||||
<AZScroller onLetterSelect={alphabetSelectorMutate} />
|
||||
<AZScroller
|
||||
onLetterSelect={(letter) =>
|
||||
alphabetSelectorMutate({
|
||||
letter,
|
||||
infiniteQuery: artistsInfiniteQuery,
|
||||
pageParams: artistPageParams,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</XStack>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react'
|
||||
import Artists from './component'
|
||||
import { ArtistsProps } from '../../screens/types'
|
||||
import Artists, { ArtistsProps } from './component'
|
||||
|
||||
export default function ArtistsScreen({
|
||||
artistsInfiniteQuery: artistInfiniteQuery,
|
||||
artistPageParams,
|
||||
showAlphabeticalSelector,
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
return (
|
||||
<Artists
|
||||
artistsInfiniteQuery={artistInfiniteQuery}
|
||||
artistPageParams={artistPageParams}
|
||||
showAlphabeticalSelector={showAlphabeticalSelector}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import SuggestedArtists from './helpers/suggested-artists'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
|
||||
export default function Index(): React.JSX.Element {
|
||||
const { refreshing, refresh, recentlyAdded, publicPlaylists, suggestedArtistsInfiniteQuery } =
|
||||
const { refreshing, refresh, publicPlaylists, suggestedArtistsInfiniteQuery } =
|
||||
useDiscoverContext()
|
||||
|
||||
return (
|
||||
@@ -23,12 +23,10 @@ export default function Index(): React.JSX.Element {
|
||||
paddingBottom={'$15'}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}
|
||||
>
|
||||
{recentlyAdded && (
|
||||
<View testID='discover-recently-added'>
|
||||
<RecentlyAdded />
|
||||
<Separator marginVertical={'$2'} />
|
||||
</View>
|
||||
)}
|
||||
<View testID='discover-recently-added'>
|
||||
<RecentlyAdded />
|
||||
<Separator marginVertical={'$2'} />
|
||||
</View>
|
||||
|
||||
{publicPlaylists && (
|
||||
<View testID='discover-public-playlists'>
|
||||
|
||||
@@ -8,15 +8,10 @@ import Icon from '../../Global/components/icon'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { useRecentlyAddedAlbums } from '../../../api/queries/album'
|
||||
|
||||
export default function RecentlyAdded(): React.JSX.Element {
|
||||
const {
|
||||
recentlyAdded,
|
||||
fetchNextRecentlyAdded,
|
||||
hasNextRecentlyAdded,
|
||||
isPendingRecentlyAdded,
|
||||
isFetchingNextRecentlyAdded,
|
||||
} = useDiscoverContext()
|
||||
const recentlyAddedAlbumsInfinityQuery = useRecentlyAddedAlbums()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
||||
|
||||
@@ -26,12 +21,7 @@ export default function RecentlyAdded(): React.JSX.Element {
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
navigation.navigate('RecentlyAdded', {
|
||||
albums: recentlyAdded,
|
||||
navigation: navigation,
|
||||
fetchNextPage: fetchNextRecentlyAdded,
|
||||
hasNextPage: hasNextRecentlyAdded,
|
||||
isPending: isPendingRecentlyAdded,
|
||||
isFetchingNextPage: isFetchingNextRecentlyAdded,
|
||||
albumsInfiniteQuery: recentlyAddedAlbumsInfinityQuery,
|
||||
})
|
||||
}}
|
||||
>
|
||||
@@ -40,7 +30,7 @@ export default function RecentlyAdded(): React.JSX.Element {
|
||||
</XStack>
|
||||
|
||||
<HorizontalCardList
|
||||
data={recentlyAdded?.slice(0, 10) ?? []}
|
||||
data={recentlyAddedAlbumsInfinityQuery.data?.slice(0, 10) ?? []}
|
||||
renderItem={({ item }) => (
|
||||
<ItemCard
|
||||
caption={item.Name}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { LayoutChangeEvent, View as RNView } from 'react-native'
|
||||
import { getToken, useTheme, View, YStack } from 'tamagui'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
@@ -12,6 +12,8 @@ import { Text } from '../helpers/text'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { useReducedHapticsSetting } from '../../../stores/settings/app'
|
||||
import { UseInfiniteQueryResult, useMutation } from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
|
||||
const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
/**
|
||||
@@ -24,7 +26,11 @@ const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
* @param onLetterSelect - Callback function to be called when a letter is selected
|
||||
* @returns A component that displays a list of letters and a selected letter overlay
|
||||
*/
|
||||
export function AZScroller({ onLetterSelect }: { onLetterSelect: (letter: string) => void }) {
|
||||
export default function AZScroller({
|
||||
onLetterSelect,
|
||||
}: {
|
||||
onLetterSelect: (letter: string) => void
|
||||
}) {
|
||||
const { width, height } = useSafeAreaFrame()
|
||||
const theme = useTheme()
|
||||
const [reducedHaptics] = useReducedHapticsSetting()
|
||||
@@ -197,3 +203,37 @@ export function AZScroller({ onLetterSelect }: { onLetterSelect: (letter: string
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const alphabeticalSelectorCallback = async (
|
||||
letter: string,
|
||||
pageParams: RefObject<Set<string>>,
|
||||
{
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isPending,
|
||||
}: UseInfiniteQueryResult<BaseItemDto[] | (string | number | BaseItemDto)[], Error>,
|
||||
) => {
|
||||
while (!pageParams.current.has(letter.toUpperCase()) && hasNextPage) {
|
||||
console.debug(`Fetching next page for alphabet selection`)
|
||||
await fetchNextPage()
|
||||
}
|
||||
console.debug(`Alphabetical Selector Callback: ${letter} complete`)
|
||||
}
|
||||
|
||||
interface AlphabetSelectorMutation {
|
||||
letter: string
|
||||
pageParams: RefObject<Set<string>>
|
||||
infiniteQuery: UseInfiniteQueryResult<BaseItemDto[] | (string | number | BaseItemDto)[], Error>
|
||||
}
|
||||
|
||||
export const useAlphabetSelector = (onSuccess: (letter: string) => void) => {
|
||||
return useMutation({
|
||||
onMutate: ({ letter }) =>
|
||||
console.debug(`Alphabet selector callback started, fetching pages for ${letter}`),
|
||||
mutationFn: ({ letter, pageParams, infiniteQuery }: AlphabetSelectorMutation) =>
|
||||
alphabeticalSelectorCallback(letter, pageParams, infiniteQuery),
|
||||
onSuccess: (data: void, { letter }: AlphabetSelectorMutation) => onSuccess(letter),
|
||||
onError: (error, { letter }) =>
|
||||
console.error(`Unable to paginate to letter ${letter}`, error),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import useAlbums from '../../../api/queries/album'
|
||||
import Albums from '../../Albums/component'
|
||||
import { useAlbumsInfiniteQueryContext } from '../../../providers/Library'
|
||||
|
||||
function AlbumsTab(): React.JSX.Element {
|
||||
const albumsInfiniteQuery = useAlbumsInfiniteQueryContext()
|
||||
const [albumPageParams, albumsInfiniteQuery] = useAlbums()
|
||||
|
||||
return (
|
||||
<Albums
|
||||
albums={albumsInfiniteQuery.data}
|
||||
fetchNextPage={albumsInfiniteQuery.fetchNextPage}
|
||||
hasNextPage={albumsInfiniteQuery.hasNextPage}
|
||||
isPending={albumsInfiniteQuery.isPending}
|
||||
isFetchingNextPage={albumsInfiniteQuery.isFetchingNextPage}
|
||||
albumsInfiniteQuery={albumsInfiniteQuery}
|
||||
showAlphabeticalSelector={true}
|
||||
albumPageParams={albumPageParams}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useAlbumArtists } from '../../../api/queries/artist'
|
||||
import Artists from '../../Artists/component'
|
||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
||||
|
||||
function ArtistsTab(): React.JSX.Element {
|
||||
const { isFavorites, sortDescending } = useLibrarySortAndFilterContext()
|
||||
|
||||
const [artistPageParams, artistsInfiniteQuery] = useAlbumArtists({
|
||||
isFavorites,
|
||||
sortDescending,
|
||||
})
|
||||
const [artistPageParams, artistsInfiniteQuery] = useAlbumArtists()
|
||||
|
||||
return (
|
||||
<Artists
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function InfoTabIndex() {
|
||||
|
||||
const { data: caption } = useQuery({
|
||||
queryKey: ['Info_Caption'],
|
||||
queryFn: () => `${pickRandomItemFromArray(INFO_CAPTIONS)}!`,
|
||||
queryFn: () => `${pickRandomItemFromArray(INFO_CAPTIONS)}`,
|
||||
staleTime: ONE_HOUR,
|
||||
initialData: 'Live and in stereo',
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@ const INFO_CAPTIONS = [
|
||||
|
||||
// Inside Jokes (that the internal Jellify team will get)
|
||||
'Thank you, Pikachu',
|
||||
'Now with 100% more nitro!', // since we use nitro modules
|
||||
'git blame violet', // lol
|
||||
|
||||
// Movie Quotes
|
||||
'Groovy, baby!', // Austin Powers
|
||||
|
||||
@@ -4,30 +4,26 @@ import {
|
||||
useInfiniteQuery,
|
||||
UseInfiniteQueryResult,
|
||||
} from '@tanstack/react-query'
|
||||
import { fetchRecentlyAdded, fetchRecentlyPlayed } from '../../api/queries/recents'
|
||||
import { fetchRecentlyPlayed } from '../../api/queries/recents'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { createContext, ReactNode, useContext, useState } from 'react'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useJellifyContext } from '..'
|
||||
import { fetchPublicPlaylists } from '../../api/queries/playlists'
|
||||
import { fetchArtistSuggestions } from '../../api/queries/suggestions'
|
||||
import { useRefetchRecentlyAdded } from '../../api/queries/album'
|
||||
|
||||
interface DiscoverContext {
|
||||
refreshing: boolean
|
||||
refresh: () => void
|
||||
recentlyAdded: BaseItemDto[] | undefined
|
||||
recentlyPlayed: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
publicPlaylists: BaseItemDto[] | undefined
|
||||
fetchNextRecentlyAdded: () => void
|
||||
fetchNextRecentlyPlayed: () => void
|
||||
fetchNextPublicPlaylists: () => void
|
||||
hasNextRecentlyAdded: boolean
|
||||
hasNextRecentlyPlayed: boolean
|
||||
hasNextPublicPlaylists: boolean
|
||||
isPendingRecentlyAdded: boolean
|
||||
isPendingRecentlyPlayed: boolean
|
||||
isPendingPublicPlaylists: boolean
|
||||
isFetchingNextRecentlyAdded: boolean
|
||||
isFetchingNextRecentlyPlayed: boolean
|
||||
isFetchingNextPublicPlaylists: boolean
|
||||
refetchPublicPlaylists: () => void
|
||||
@@ -38,21 +34,7 @@ const DiscoverContextInitializer = () => {
|
||||
const { api, library, user } = useJellifyContext()
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
|
||||
const {
|
||||
data: recentlyAdded,
|
||||
refetch: refetchRecentlyAdded,
|
||||
fetchNextPage: fetchNextRecentlyAdded,
|
||||
hasNextPage: hasNextRecentlyAdded,
|
||||
isPending: isPendingRecentlyAdded,
|
||||
isFetchingNextPage: isFetchingNextRecentlyAdded,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
const refetchRecentlyAdded = useRefetchRecentlyAdded()
|
||||
|
||||
const {
|
||||
data: publicPlaylists,
|
||||
@@ -111,19 +93,14 @@ const DiscoverContextInitializer = () => {
|
||||
return {
|
||||
refreshing,
|
||||
refresh,
|
||||
recentlyAdded,
|
||||
recentlyPlayed,
|
||||
publicPlaylists,
|
||||
fetchNextRecentlyAdded,
|
||||
fetchNextRecentlyPlayed,
|
||||
fetchNextPublicPlaylists,
|
||||
hasNextRecentlyAdded,
|
||||
hasNextRecentlyPlayed,
|
||||
hasNextPublicPlaylists,
|
||||
isPendingRecentlyAdded,
|
||||
isPendingRecentlyPlayed,
|
||||
isPendingPublicPlaylists,
|
||||
isFetchingNextRecentlyAdded,
|
||||
isFetchingNextRecentlyPlayed,
|
||||
isFetchingNextPublicPlaylists,
|
||||
refetchPublicPlaylists,
|
||||
@@ -134,19 +111,14 @@ const DiscoverContextInitializer = () => {
|
||||
const DiscoverContext = createContext<DiscoverContext>({
|
||||
refreshing: false,
|
||||
refresh: () => {},
|
||||
recentlyAdded: undefined,
|
||||
recentlyPlayed: undefined,
|
||||
publicPlaylists: undefined,
|
||||
fetchNextRecentlyAdded: () => {},
|
||||
fetchNextRecentlyPlayed: () => {},
|
||||
fetchNextPublicPlaylists: () => {},
|
||||
hasNextRecentlyAdded: false,
|
||||
hasNextRecentlyPlayed: false,
|
||||
hasNextPublicPlaylists: false,
|
||||
isPendingRecentlyAdded: false,
|
||||
isPendingRecentlyPlayed: false,
|
||||
isPendingPublicPlaylists: false,
|
||||
isFetchingNextRecentlyAdded: false,
|
||||
isFetchingNextRecentlyPlayed: false,
|
||||
isFetchingNextPublicPlaylists: false,
|
||||
refetchPublicPlaylists: () => {},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { BaseItemDto, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useJellifyContext } from '..'
|
||||
import { RefObject, useMemo, useRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import QueryConfig from '../../api/queries/query.config'
|
||||
import { fetchTracks } from '../../api/queries/tracks'
|
||||
import { fetchAlbums } from '../../api/queries/album'
|
||||
import { useLibrarySortAndFilterContext } from './sorting-filtering'
|
||||
import { fetchUserPlaylists } from '../../api/queries/playlists'
|
||||
import { createContext, useContextSelector } from 'use-context-selector'
|
||||
@@ -17,12 +16,9 @@ import {
|
||||
export const alphabet = '#abcdefghijklmnopqrstuvwxyz'.split('')
|
||||
|
||||
interface LibraryContext {
|
||||
albumsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
tracksInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
// genres: BaseItemDto[] | undefined
|
||||
playlistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
|
||||
|
||||
albumPageParams: RefObject<string[]>
|
||||
}
|
||||
|
||||
type LibraryPage = {
|
||||
@@ -35,8 +31,6 @@ const LibraryContextInitializer = () => {
|
||||
|
||||
const { sortDescending, isFavorites } = useLibrarySortAndFilterContext()
|
||||
|
||||
const albumPageParams = useRef<string[]>([])
|
||||
|
||||
const tracksInfiniteQuery = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.AllTracks, isFavorites, sortDescending, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) =>
|
||||
@@ -59,45 +53,6 @@ const LibraryContextInitializer = () => {
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
})
|
||||
|
||||
const albumsInfiniteQuery = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.AllAlbumsAlphabetical, isFavorites, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchAlbums(
|
||||
api,
|
||||
user,
|
||||
library,
|
||||
pageParam,
|
||||
isFavorites,
|
||||
[ItemSortBy.SortName],
|
||||
[SortOrder.Ascending],
|
||||
),
|
||||
initialPageParam: alphabet[0],
|
||||
select: (data) => data.pages.flatMap((page) => [page.title, ...page.data]),
|
||||
maxPages: alphabet.length,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug(`Albums last page length: ${lastPage.data.length}`)
|
||||
if (lastPageParam !== alphabet[alphabet.length - 1]) {
|
||||
albumPageParams.current = [
|
||||
...allPageParams,
|
||||
alphabet[alphabet.indexOf(lastPageParam) + 1],
|
||||
]
|
||||
return alphabet[alphabet.indexOf(lastPageParam) + 1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => {
|
||||
console.debug(`Albums first page: ${firstPage.title}`)
|
||||
albumPageParams.current = allPageParams
|
||||
if (firstPageParam !== alphabet[0]) {
|
||||
albumPageParams.current = allPageParams
|
||||
return alphabet[alphabet.indexOf(firstPageParam) - 1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
|
||||
const playlistsInfiniteQuery = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.Playlists, library?.playlistLibraryId],
|
||||
queryFn: () => fetchUserPlaylists(api, user, library),
|
||||
@@ -110,59 +65,11 @@ const LibraryContextInitializer = () => {
|
||||
|
||||
return {
|
||||
tracksInfiniteQuery,
|
||||
albumsInfiniteQuery,
|
||||
albumPageParams,
|
||||
playlistsInfiniteQuery,
|
||||
}
|
||||
}
|
||||
|
||||
const LibraryContext = createContext<LibraryContext>({
|
||||
albumPageParams: { current: [] },
|
||||
albumsInfiniteQuery: {
|
||||
data: undefined,
|
||||
error: null,
|
||||
isEnabled: true,
|
||||
isStale: false,
|
||||
isRefetching: false,
|
||||
isError: false,
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
isFetching: true,
|
||||
isSuccess: false,
|
||||
isFetched: false,
|
||||
hasPreviousPage: false,
|
||||
refetch: async () =>
|
||||
Promise.resolve(
|
||||
{} as InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>,
|
||||
),
|
||||
fetchNextPage: async () =>
|
||||
Promise.resolve(
|
||||
{} as InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>,
|
||||
),
|
||||
hasNextPage: false,
|
||||
isFetchingNextPage: false,
|
||||
isFetchPreviousPageError: false,
|
||||
isFetchNextPageError: false,
|
||||
isFetchingPreviousPage: false,
|
||||
isLoadingError: false,
|
||||
isRefetchError: false,
|
||||
isPlaceholderData: false,
|
||||
status: 'pending',
|
||||
fetchStatus: 'idle',
|
||||
dataUpdatedAt: 0,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
errorUpdateCount: 0,
|
||||
isFetchedAfterMount: false,
|
||||
isInitialLoading: false,
|
||||
isPaused: false,
|
||||
fetchPreviousPage: async () =>
|
||||
Promise.resolve(
|
||||
{} as InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>,
|
||||
),
|
||||
promise: Promise.resolve([]),
|
||||
},
|
||||
tracksInfiniteQuery: {
|
||||
data: undefined,
|
||||
error: null,
|
||||
@@ -257,8 +164,6 @@ export const LibraryProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
[
|
||||
context.tracksInfiniteQuery.data,
|
||||
context.tracksInfiniteQuery.isPending,
|
||||
context.albumsInfiniteQuery.data,
|
||||
context.albumsInfiniteQuery.isPending,
|
||||
context.playlistsInfiniteQuery.data,
|
||||
context.playlistsInfiniteQuery.isPending,
|
||||
],
|
||||
@@ -268,10 +173,6 @@ export const LibraryProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
|
||||
export const useTracksInfiniteQueryContext = () =>
|
||||
useContextSelector(LibraryContext, (context) => context.tracksInfiniteQuery)
|
||||
export const useAlbumsInfiniteQueryContext = () =>
|
||||
useContextSelector(LibraryContext, (context) => context.albumsInfiniteQuery)
|
||||
export const useAlbumPageParamsContext = () =>
|
||||
useContextSelector(LibraryContext, (context) => context.albumPageParams)
|
||||
export const usePlaylistsInfiniteQueryContext = () =>
|
||||
useContextSelector(LibraryContext, (context) => context.playlistsInfiniteQuery)
|
||||
|
||||
|
||||
@@ -2,18 +2,10 @@ import { RouteProp } from '@react-navigation/native'
|
||||
import Albums from '../../components/Albums/component'
|
||||
import DiscoverStackParamList, { RecentlyAddedProps } from './types'
|
||||
|
||||
export default function RecentlyAdded({
|
||||
route,
|
||||
}: {
|
||||
route: RouteProp<DiscoverStackParamList, 'RecentlyAdded'>
|
||||
}): React.JSX.Element {
|
||||
export default function RecentlyAdded({ route }: RecentlyAddedProps): React.JSX.Element {
|
||||
return (
|
||||
<Albums
|
||||
albums={route.params.albums}
|
||||
fetchNextPage={route.params.fetchNextPage}
|
||||
hasNextPage={route.params.hasNextPage}
|
||||
isPending={route.params.isPending}
|
||||
isFetchingNextPage={route.params.isFetchingNextPage}
|
||||
albumsInfiniteQuery={route.params.albumsInfiniteQuery}
|
||||
showAlphabeticalSelector={false}
|
||||
/>
|
||||
)
|
||||
|
||||
7
src/screens/Discover/types.d.ts
vendored
7
src/screens/Discover/types.d.ts
vendored
@@ -6,12 +6,7 @@ import { UseInfiniteQueryResult } from '@tanstack/react-query'
|
||||
type DiscoverStackParamList = BaseStackParamList & {
|
||||
Discover: undefined
|
||||
RecentlyAdded: {
|
||||
albums: BaseItemDto[] | undefined
|
||||
navigation: NativeStackNavigationProp<RootStackParamList>
|
||||
fetchNextPage: () => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
isFetchingNextPage: boolean
|
||||
albumsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
|
||||
}
|
||||
PublicPlaylists: {
|
||||
playlists: BaseItemDto[] | undefined
|
||||
|
||||
9
src/screens/types.d.ts
vendored
9
src/screens/types.d.ts
vendored
@@ -83,15 +83,6 @@ export type ContextProps = NativeStackScreenProps<RootStackParamList, 'Context'>
|
||||
export type AddToPlaylistProps = NativeStackScreenProps<RootStackParamList, 'AddToPlaylist'>
|
||||
export type AudioSpecsProps = NativeStackScreenProps<RootStackParamList, 'AudioSpecs'>
|
||||
|
||||
export type ArtistsProps = {
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<
|
||||
BaseItemDto[] | (string | number | BaseItemDto)[],
|
||||
Error
|
||||
>
|
||||
showAlphabeticalSelector: boolean
|
||||
artistPageParams?: RefObject<Set<string>>
|
||||
}
|
||||
|
||||
export type GenresProps = {
|
||||
genres: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
|
||||
|
||||
44
src/utils/query-selectors.ts
Normal file
44
src/utils/query-selectors.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { InfiniteData } from '@tanstack/react-query'
|
||||
import { isString } from 'lodash'
|
||||
import { RefObject } from 'react'
|
||||
|
||||
export default function flattenInfiniteQueryPages(
|
||||
data: InfiniteData<BaseItemDto[], unknown>,
|
||||
pageParams: RefObject<Set<string>>,
|
||||
) {
|
||||
/**
|
||||
* A flattened array of all artists derived from the infinite query
|
||||
*/
|
||||
const flattenedItemPages = data.pages.flatMap((page) => page)
|
||||
|
||||
/**
|
||||
* A set of letters we've seen so we can add them to the alphabetical selector
|
||||
*/
|
||||
const seenLetters = new Set<string>()
|
||||
|
||||
/**
|
||||
* The final array that will be provided to and rendered by the {@link Artists} component
|
||||
*/
|
||||
const flashListItems: (string | number | BaseItemDto)[] = []
|
||||
|
||||
flattenedItemPages.forEach((item: BaseItemDto) => {
|
||||
const rawLetter = isString(item.SortName) ? item.SortName.charAt(0).toUpperCase() : '#'
|
||||
|
||||
/**
|
||||
* An alpha character or a hash if the artist's name doesn't start with a letter
|
||||
*/
|
||||
const letter = rawLetter.match(/[A-Z]/) ? rawLetter : '#'
|
||||
|
||||
if (!seenLetters.has(letter)) {
|
||||
seenLetters.add(letter.toUpperCase())
|
||||
flashListItems.push(letter.toUpperCase())
|
||||
}
|
||||
|
||||
flashListItems.push(item)
|
||||
})
|
||||
|
||||
pageParams.current = seenLetters
|
||||
|
||||
return flashListItems
|
||||
}
|
||||
199
yarn.lock
199
yarn.lock
@@ -1420,6 +1420,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.0.1.tgz#0d32f1bbfba511948ad247ab01b9007724fc9f52"
|
||||
integrity sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==
|
||||
|
||||
"@jest/get-type@30.1.0":
|
||||
version "30.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc"
|
||||
integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==
|
||||
|
||||
"@jest/globals@30.0.5":
|
||||
version "30.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-30.0.5.tgz#ca70e0ac08ab40417cf8cd92bcb76116c2ccca63"
|
||||
@@ -1924,10 +1929,10 @@
|
||||
dependencies:
|
||||
"@react-native-vector-icons/common" "^12.3.0"
|
||||
|
||||
"@react-native/assets-registry@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.81.0.tgz#ff28654b6e64164137d10de7333da05b3d994f2c"
|
||||
integrity sha512-rZs8ziQ1YRV3Z5Mw5AR7YcgI3q1Ya9NIx6nyuZAT9wDSSjspSi+bww+Hargh/a4JfV2Ajcxpn9X9UiFJr1ddPw==
|
||||
"@react-native/assets-registry@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.81.1.tgz#94993d165b79feeec09432f867ea2edc8a307e60"
|
||||
integrity sha512-o/AeHeoiPW8x9MzxE1RSnKYc+KZMW9b7uaojobEz0G8fKgGD1R8n5CJSOiQ/0yO2fJdC5wFxMMOgy2IKwRrVgw==
|
||||
|
||||
"@react-native/babel-plugin-codegen@0.81.0":
|
||||
version "0.81.0"
|
||||
@@ -1999,12 +2004,25 @@
|
||||
nullthrows "^1.1.1"
|
||||
yargs "^17.6.2"
|
||||
|
||||
"@react-native/community-cli-plugin@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.0.tgz#16407f0eb71fd251ec08536085e4dbda83279d56"
|
||||
integrity sha512-n04ACkCaLR54NmA/eWiDpjC16pHr7+yrbjQ6OEdRoXbm5EfL8FEre2kDAci7pfFdiSMpxdRULDlKpfQ+EV/GAQ==
|
||||
"@react-native/codegen@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.81.1.tgz#6cbe4dbe0c85a260c1fb7dce301234f527771cd6"
|
||||
integrity sha512-8KoUE1j65fF1PPHlAhSeUHmcyqpE+Z7Qv27A89vSZkz3s8eqWSRu2hZtCl0D3nSgS0WW0fyrIsFaRFj7azIiPw==
|
||||
dependencies:
|
||||
"@react-native/dev-middleware" "0.81.0"
|
||||
"@babel/core" "^7.25.2"
|
||||
"@babel/parser" "^7.25.3"
|
||||
glob "^7.1.1"
|
||||
hermes-parser "0.29.1"
|
||||
invariant "^2.2.4"
|
||||
nullthrows "^1.1.1"
|
||||
yargs "^17.6.2"
|
||||
|
||||
"@react-native/community-cli-plugin@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.1.tgz#83110c7e839e9385b8ac5108f3c5600ce9db4f94"
|
||||
integrity sha512-FuIpZcjBiiYcVMNx+1JBqTPLs2bUIm6X4F5enYGYcetNE2nfSMUVO8SGUtTkBdbUTfKesXYSYN8wungyro28Ag==
|
||||
dependencies:
|
||||
"@react-native/dev-middleware" "0.81.1"
|
||||
debug "^4.4.0"
|
||||
invariant "^2.2.4"
|
||||
metro "^0.83.1"
|
||||
@@ -2012,18 +2030,18 @@
|
||||
metro-core "^0.83.1"
|
||||
semver "^7.1.3"
|
||||
|
||||
"@react-native/debugger-frontend@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.81.0.tgz#a032e98896371095919fa04b8ac93a1d1fe96f72"
|
||||
integrity sha512-N/8uL2CGQfwiQRYFUNfmaYxRDSoSeOmFb56rb0PDnP3XbS5+X9ee7X4bdnukNHLGfkRdH7sVjlB8M5zE8XJOhw==
|
||||
"@react-native/debugger-frontend@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.81.1.tgz#db71318e9cfe973cd731c59d2361700b8422a304"
|
||||
integrity sha512-dwKv1EqKD+vONN4xsfyTXxn291CNl1LeBpaHhNGWASK1GO4qlyExMs4TtTjN57BnYHikR9PzqPWcUcfzpVRaLg==
|
||||
|
||||
"@react-native/dev-middleware@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.81.0.tgz#5f4018bdca027feb903cb2902d48204c0703587c"
|
||||
integrity sha512-J/HeC/+VgRyGECPPr9rAbe5S0OL6MCIrvrC/kgNKSME5+ZQLCiTpt3pdAoAMXwXiF9a02Nmido0DnyM1acXTIA==
|
||||
"@react-native/dev-middleware@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.81.1.tgz#3a14f416a2fc80d4f993e22bcb84ad781ac4e638"
|
||||
integrity sha512-hy3KlxNOfev3O5/IuyZSstixWo7E9FhljxKGHdvVtZVNjQdM+kPMh66mxeJbB2TjdJGAyBT4DjIwBaZnIFOGHQ==
|
||||
dependencies:
|
||||
"@isaacs/ttlcache" "^1.4.1"
|
||||
"@react-native/debugger-frontend" "0.81.0"
|
||||
"@react-native/debugger-frontend" "0.81.1"
|
||||
chrome-launcher "^0.15.2"
|
||||
chromium-edge-launcher "^0.2.0"
|
||||
connect "^3.6.5"
|
||||
@@ -2057,16 +2075,21 @@
|
||||
resolved "https://registry.yarnpkg.com/@react-native/eslint-plugin/-/eslint-plugin-0.81.0.tgz#5a236c92394f44f4cbfe400d7b87a7e25599dd54"
|
||||
integrity sha512-kNSraBk1BuW21raXRJp8+BlTJwnpU96kRNQ9YNxfcY78k9zOH2YXiYsK0SfrDrdcl5kspiXRSj3Rueh6jvDRHw==
|
||||
|
||||
"@react-native/gradle-plugin@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.81.0.tgz#6a9b0583f5f21142ddaeca72ef3e81160a8e3ce8"
|
||||
integrity sha512-LGNtPXO1RKLws5ORRb4Q4YULi2qxM4qZRuARtwqM/1f2wyZVggqapoV0OXlaXaz+GiEd2ll3ROE4CcLN6J93jg==
|
||||
"@react-native/gradle-plugin@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.81.1.tgz#a7afdc962c298acf6a99142e6db78b554aba6006"
|
||||
integrity sha512-RpRxs/LbWVM9Zi5jH1qBLgTX746Ei+Ui4vj3FmUCd9EXUSECM5bJpphcsvqjxM5Vfl/o2wDLSqIoFkVP/6Te7g==
|
||||
|
||||
"@react-native/js-polyfills@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.81.0.tgz#81900a25b626e9bca8b38b545b6987695d469d59"
|
||||
integrity sha512-whXZWIogzoGpqdyTjqT89M6DXmlOkWqNpWoVOAwVi8XFCMO+L7WTk604okIgO6gdGZcP1YtFpQf9JusbKrv/XA==
|
||||
|
||||
"@react-native/js-polyfills@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.81.1.tgz#066343aca3d3aaf846335492c7114e08e9a0e975"
|
||||
integrity sha512-w093OkHFfCnJKnkiFizwwjgrjh5ra53BU0ebPM3uBLkIQ6ZMNSCTZhG8ZHIlAYeIGtEinvmnSUi3JySoxuDCAQ==
|
||||
|
||||
"@react-native/metro-babel-transformer@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.81.0.tgz#f17f104f53d9976ba8a3f26c3d13dfc4f3800b54"
|
||||
@@ -2092,20 +2115,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.1.0.tgz#939b87a9849e81687d3640c5efa2a486ac266f91"
|
||||
integrity sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==
|
||||
|
||||
"@react-native/normalize-colors@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.81.0.tgz#538db4d0b9378b73d3be009e99d44cf78c12baf7"
|
||||
integrity sha512-3gEu/29uFgz+81hpUgdlOojM4rjHTIPwxpfygFNY60V6ywZih3eLDTS8kAjNZfPFHQbcYrNorJzwnL5yFF/uLw==
|
||||
"@react-native/normalize-colors@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.81.1.tgz#bf290526e1bcbb8d14e20b509ca1030d5df71585"
|
||||
integrity sha512-TsaeZlE8OYFy3PSWc+1VBmAzI2T3kInzqxmwXoGU4w1d4XFkQFg271Ja9GmDi9cqV3CnBfqoF9VPwRxVlc/l5g==
|
||||
|
||||
"@react-native/typescript-config@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/typescript-config/-/typescript-config-0.81.0.tgz#d25dd746ac320293cd10bb8302489ec383bdabe2"
|
||||
integrity sha512-BnmmXHafGitDBD5naQF1wwaJ2LY1CLMABs009tVTF4ZOPK9/IrGdoNjuiI+tjHAeug6S68MlSNyVxknZ2JBIvw==
|
||||
|
||||
"@react-native/virtualized-lists@0.81.0":
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.81.0.tgz#962ea39af006e58bfe898bb54c164b52075d491f"
|
||||
integrity sha512-p14QC5INHkbMZ96158sUxkSwN6zp138W11G+CRGoLJY4Q9WRJBCe7wHR5Owyy3XczQXrIih/vxAXwgYeZ2XByg==
|
||||
"@react-native/virtualized-lists@0.81.1":
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.81.1.tgz#b550d54a0762e85b88ba9be0b32a1675664f92ed"
|
||||
integrity sha512-yG+zcMtyApW1yRwkNFvlXzEg3RIFdItuwr/zEvPCSdjaL+paX4rounpL0YX5kS9MsDIE5FXfcqINXg7L0xuwPg==
|
||||
dependencies:
|
||||
invariant "^2.2.4"
|
||||
nullthrows "^1.1.1"
|
||||
@@ -3379,52 +3402,52 @@
|
||||
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.5":
|
||||
version "5.85.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.85.5.tgz#4e12cea74665088e9e5f70c0046e51e7b5baba0e"
|
||||
integrity sha512-E1N+eMPWfV0PwTNa8tRqyOgIzFJSGvrC5hVIxNehLL/jucPvLi0QUlIG/KC4Vg6jVarONSLhONCM4dkSugEUFw==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@tanstack/query-persist-client-core" "5.85.5"
|
||||
"@tanstack/query-persist-client-core" "5.85.6"
|
||||
|
||||
"@tanstack/query-core@5.85.5":
|
||||
version "5.85.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.85.5.tgz#c4adc126bb3a927e4d60280bf3cf62210700147c"
|
||||
integrity sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==
|
||||
"@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-persist-client-core@5.85.5":
|
||||
version "5.85.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.85.5.tgz#04ba995509d5e279771b37c0e0a21879476f3576"
|
||||
integrity sha512-2JQiyiTVaaUu8pwPqOp6tjNa64ZN+0T9eZ3lfksV4le1VuG99fTcAYmZFIydvzwWlSM7GEF/1kpl5bwW2Y1qfQ==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.85.5"
|
||||
"@tanstack/query-core" "5.85.6"
|
||||
|
||||
"@tanstack/react-query-persist-client@^5.85.5":
|
||||
version "5.85.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.85.5.tgz#0fa1e581b3a7564a49e097a26c69dea009b33724"
|
||||
integrity sha512-KzISZPtJtWZAwH/Ln1FclaiHVwdeV04WX7wUYLe1vw7zyfcPljHeyXlmVf8nxhFm8ujMBdGQVzP2iNn6ehzjQA==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@tanstack/query-persist-client-core" "5.85.5"
|
||||
"@tanstack/query-persist-client-core" "5.85.6"
|
||||
|
||||
"@tanstack/react-query@^5.85.5":
|
||||
version "5.85.5"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.85.5.tgz#50a1c02b50a59f93eba8f0d91d54d39c6c534c5e"
|
||||
integrity sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.85.5"
|
||||
"@tanstack/query-core" "5.85.6"
|
||||
|
||||
"@telemetrydeck/sdk@^2.0.4":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@telemetrydeck/sdk/-/sdk-2.0.4.tgz#f846151784fbc165280c74db830a91865b1381c1"
|
||||
integrity sha512-x4S83AqSo6wvLJ6nRYdyJEqd9qmblUdBgsTRrjH5z++b9pnf2NMc8NpVAa48KIB1pRuP/GTGzXxVYdNoie/DVg==
|
||||
|
||||
"@testing-library/react-native@^13.2.2":
|
||||
version "13.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-13.2.2.tgz#8de8e6e145b8a10338f997ff7b739b79eb0b7c98"
|
||||
integrity sha512-QALF+nZ4BSXBOtUs5ljLnaHKuyR+ykakYB3RYwciSrllhgZkbUjXeGkugCxrmEtQ2BUZnYVRY7AEGboMP/hucg==
|
||||
"@testing-library/react-native@^13.2.3":
|
||||
version "13.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-13.3.3.tgz#4bf02911c4e18075df40b5de0e029c209fb45bda"
|
||||
integrity sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
jest-matcher-utils "^30.0.2"
|
||||
pretty-format "^30.0.2"
|
||||
jest-matcher-utils "^30.0.5"
|
||||
picocolors "^1.1.1"
|
||||
pretty-format "^30.0.5"
|
||||
redent "^3.0.0"
|
||||
|
||||
"@tybys/wasm-util@^0.9.0":
|
||||
@@ -6617,6 +6640,16 @@ jest-diff@30.0.5:
|
||||
chalk "^4.1.2"
|
||||
pretty-format "30.0.5"
|
||||
|
||||
jest-diff@30.1.1:
|
||||
version "30.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.1.1.tgz#cfe8327c059178affac17d4c003e7096ad19583c"
|
||||
integrity sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==
|
||||
dependencies:
|
||||
"@jest/diff-sequences" "30.0.1"
|
||||
"@jest/get-type" "30.1.0"
|
||||
chalk "^4.1.2"
|
||||
pretty-format "30.0.5"
|
||||
|
||||
jest-docblock@30.0.1:
|
||||
version "30.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-30.0.1.tgz#545ff59f2fa88996bd470dba7d3798a8421180b1"
|
||||
@@ -6720,7 +6753,7 @@ jest-matcher-utils@30.0.4:
|
||||
jest-diff "30.0.4"
|
||||
pretty-format "30.0.2"
|
||||
|
||||
jest-matcher-utils@30.0.5, jest-matcher-utils@^30.0.2:
|
||||
jest-matcher-utils@30.0.5:
|
||||
version "30.0.5"
|
||||
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz#dff3334be58faea4a5e1becc228656fbbfc2467d"
|
||||
integrity sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==
|
||||
@@ -6730,6 +6763,16 @@ jest-matcher-utils@30.0.5, jest-matcher-utils@^30.0.2:
|
||||
jest-diff "30.0.5"
|
||||
pretty-format "30.0.5"
|
||||
|
||||
jest-matcher-utils@^30.0.5:
|
||||
version "30.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.1.1.tgz#e45419d966cd2e5e7d7ade6da747035c6a3b8afc"
|
||||
integrity sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==
|
||||
dependencies:
|
||||
"@jest/get-type" "30.1.0"
|
||||
chalk "^4.1.2"
|
||||
jest-diff "30.1.1"
|
||||
pretty-format "30.0.5"
|
||||
|
||||
jest-message-util@30.0.2:
|
||||
version "30.0.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.0.2.tgz#9dfdc37570d172f0ffdc42a0318036ff4008837f"
|
||||
@@ -7942,10 +7985,10 @@ open@^7.0.3, open@^7.4.2:
|
||||
is-docker "^2.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
openai@^5.12.2:
|
||||
version "5.12.2"
|
||||
resolved "https://registry.yarnpkg.com/openai/-/openai-5.12.2.tgz#512ab6b80eb8414837436e208f1b951442b97761"
|
||||
integrity sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==
|
||||
openai@^5.16.0:
|
||||
version "5.16.0"
|
||||
resolved "https://registry.yarnpkg.com/openai/-/openai-5.16.0.tgz#a302d4ca92954598c79c72dd199c58994708130d"
|
||||
integrity sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw==
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.4"
|
||||
@@ -8252,7 +8295,7 @@ pretty-format@30.0.2, pretty-format@^30.0.0:
|
||||
ansi-styles "^5.2.0"
|
||||
react-is "^18.3.1"
|
||||
|
||||
pretty-format@30.0.5, pretty-format@^30.0.2:
|
||||
pretty-format@30.0.5, pretty-format@^30.0.5:
|
||||
version "30.0.5"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.0.5.tgz#e001649d472800396c1209684483e18a4d250360"
|
||||
integrity sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==
|
||||
@@ -8594,19 +8637,19 @@ react-native-worklets@0.4.1:
|
||||
"@babel/preset-typescript" "^7.16.7"
|
||||
convert-source-map "^2.0.0"
|
||||
|
||||
react-native@0.81.0:
|
||||
version "0.81.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.0.tgz#ebb645f3fb2fc2ffb222d2f294ca4e81e6568f15"
|
||||
integrity sha512-RDWhewHGsAa5uZpwIxnJNiv5tW2y6/DrQUjEBdAHPzGMwuMTshern2s4gZaWYeRU3SQguExVddCjiss9IBhxqA==
|
||||
react-native@0.81.1:
|
||||
version "0.81.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.81.1.tgz#0825cde0cc00d569cbec7d2fa1abd38a66885250"
|
||||
integrity sha512-k2QJzWc/CUOwaakmD1SXa4uJaLcwB2g2V9BauNIjgtXYYAeyFjx9jlNz/+wAEcHLg9bH5mgMdeAwzvXqjjh9Hg==
|
||||
dependencies:
|
||||
"@jest/create-cache-key-function" "^29.7.0"
|
||||
"@react-native/assets-registry" "0.81.0"
|
||||
"@react-native/codegen" "0.81.0"
|
||||
"@react-native/community-cli-plugin" "0.81.0"
|
||||
"@react-native/gradle-plugin" "0.81.0"
|
||||
"@react-native/js-polyfills" "0.81.0"
|
||||
"@react-native/normalize-colors" "0.81.0"
|
||||
"@react-native/virtualized-lists" "0.81.0"
|
||||
"@react-native/assets-registry" "0.81.1"
|
||||
"@react-native/codegen" "0.81.1"
|
||||
"@react-native/community-cli-plugin" "0.81.1"
|
||||
"@react-native/gradle-plugin" "0.81.1"
|
||||
"@react-native/js-polyfills" "0.81.1"
|
||||
"@react-native/normalize-colors" "0.81.1"
|
||||
"@react-native/virtualized-lists" "0.81.1"
|
||||
abort-controller "^3.0.0"
|
||||
anser "^1.4.9"
|
||||
ansi-regex "^5.0.0"
|
||||
|
||||
Reference in New Issue
Block a user