mirror of
https://github.com/Jellify-Music/App.git
synced 2025-12-20 10:10:08 -06:00
Discover Refactor (#752)
* Discover Refactor tear down discover context provider add animations to rows in discover screen tidy up home screen refresh * handle case where teh discover screen is empty on initial load
This commit is contained in:
1
bun.lock
1
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jellify",
|
"name": "jellify",
|
||||||
|
|||||||
23
src/api/mutations/discover/index.ts
Normal file
23
src/api/mutations/discover/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
import { useRecentlyAddedAlbums } from '../../queries/album'
|
||||||
|
import { usePublicPlaylists } from '../../queries/playlist'
|
||||||
|
import { useDiscoverArtists } from '../../queries/suggestions'
|
||||||
|
|
||||||
|
const useDiscoverQueries = () => {
|
||||||
|
const { refetch: refetchRecentlyAdded } = useRecentlyAddedAlbums()
|
||||||
|
|
||||||
|
const { refetch: refetchPublicPlaylists } = usePublicPlaylists()
|
||||||
|
|
||||||
|
const { refetch: refetchArtistSuggestions } = useDiscoverArtists()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () =>
|
||||||
|
await Promise.allSettled([
|
||||||
|
refetchRecentlyAdded(),
|
||||||
|
refetchPublicPlaylists(),
|
||||||
|
refetchArtistSuggestions(),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDiscoverQueries
|
||||||
@@ -16,12 +16,12 @@ const useHomeQueries = () => {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await Promise.all([
|
await Promise.allSettled([
|
||||||
refetchRecentlyPlayed(),
|
refetchRecentlyPlayed(),
|
||||||
refetchFrequentlyPlayed(),
|
refetchFrequentlyPlayed(),
|
||||||
refetchUserPlaylists(),
|
refetchUserPlaylists(),
|
||||||
])
|
])
|
||||||
await Promise.all([refetchFrequentArtists(), refetchRecentArtists()])
|
await Promise.allSettled([refetchFrequentArtists(), refetchRecentArtists()])
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,3 +39,17 @@ export const usePlaylistTracks = (playlist: BaseItemDto) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePublicPlaylists = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId],
|
||||||
|
queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam),
|
||||||
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||||
|
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||||
|
initialPageParam: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
42
src/api/queries/suggestions/index.ts
Normal file
42
src/api/queries/suggestions/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||||
|
import { SuggestionQueryKeys } from './keys'
|
||||||
|
import { fetchArtistSuggestions, fetchSearchSuggestions } from './utils/suggestions'
|
||||||
|
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||||
|
import { isUndefined } from 'lodash'
|
||||||
|
|
||||||
|
export const useSearchSuggestions = () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [SuggestionQueryKeys.SearchSuggestions, library?.musicLibraryId],
|
||||||
|
queryFn: () => fetchSearchSuggestions(api, user, library?.musicLibraryId),
|
||||||
|
enabled: !isUndefined(library),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDiscoverArtists = () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: [
|
||||||
|
SuggestionQueryKeys.InfiniteArtistSuggestions,
|
||||||
|
user?.id,
|
||||||
|
library?.musicLibraryId,
|
||||||
|
],
|
||||||
|
queryFn: ({ pageParam }) =>
|
||||||
|
fetchArtistSuggestions(api, user, library?.musicLibraryId, pageParam),
|
||||||
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||||
|
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||||
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
|
initialPageParam: 0,
|
||||||
|
maxPages: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
4
src/api/queries/suggestions/keys.ts
Normal file
4
src/api/queries/suggestions/keys.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum SuggestionQueryKeys {
|
||||||
|
InfiniteArtistSuggestions,
|
||||||
|
SearchSuggestions,
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
|||||||
import { BaseItemDto, BaseItemKind, ItemFields } from '@jellyfin/sdk/lib/generated-client/models'
|
import { BaseItemDto, BaseItemKind, ItemFields } from '@jellyfin/sdk/lib/generated-client/models'
|
||||||
import { Api } from '@jellyfin/sdk'
|
import { Api } from '@jellyfin/sdk'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
import { JellifyUser } from '../../types/JellifyUser'
|
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches search suggestions from the Jellyfin server
|
* Fetches search suggestions from the Jellyfin server
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { getToken, ScrollView, useTheme, View, YStack } from 'tamagui'
|
import { getToken, ScrollView, useTheme, YStack } from 'tamagui'
|
||||||
import RecentlyAdded from './helpers/just-added'
|
import RecentlyAdded from './helpers/just-added'
|
||||||
import { useDiscoverContext } from '../../providers/Discover'
|
|
||||||
import { RefreshControl } from 'react-native'
|
import { RefreshControl } from 'react-native'
|
||||||
import PublicPlaylists from './helpers/public-playlists'
|
import PublicPlaylists from './helpers/public-playlists'
|
||||||
import SuggestedArtists from './helpers/suggested-artists'
|
import SuggestedArtists from './helpers/suggested-artists'
|
||||||
|
import useDiscoverQueries from '../../api/mutations/discover'
|
||||||
|
import { useIsRestoring } from '@tanstack/react-query'
|
||||||
|
import { useRecentlyAddedAlbums } from '../../api/queries/album'
|
||||||
|
|
||||||
export default function Index(): React.JSX.Element {
|
export default function Index(): React.JSX.Element {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const { refreshing, refresh, publicPlaylists, suggestedArtistsInfiniteQuery } =
|
const { mutateAsync: refreshAsync, isPending: refreshing } = useDiscoverQueries()
|
||||||
useDiscoverContext()
|
|
||||||
|
|
||||||
const publicPlaylistsLength = (publicPlaylists ?? []).length
|
const isRestoring = useIsRestoring()
|
||||||
|
|
||||||
|
const { isPending: loadingInitialData } = useRecentlyAddedAlbums()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -25,29 +28,25 @@ export default function Index(): React.JSX.Element {
|
|||||||
removeClippedSubviews
|
removeClippedSubviews
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing || isRestoring || loadingInitialData}
|
||||||
onRefresh={refresh}
|
onRefresh={refreshAsync}
|
||||||
tintColor={theme.primary.val}
|
tintColor={theme.primary.val}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<YStack gap={'$3'}>
|
<DiscoverContent />
|
||||||
<View testID='discover-recently-added'>
|
|
||||||
<RecentlyAdded />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{publicPlaylistsLength > 0 && (
|
|
||||||
<View testID='discover-public-playlists'>
|
|
||||||
<PublicPlaylists />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{suggestedArtistsInfiniteQuery.data && (
|
|
||||||
<View testID='discover-suggested-artists'>
|
|
||||||
<SuggestedArtists />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</YStack>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DiscoverContent() {
|
||||||
|
return (
|
||||||
|
<YStack gap={'$3'}>
|
||||||
|
<RecentlyAdded />
|
||||||
|
|
||||||
|
<PublicPlaylists />
|
||||||
|
|
||||||
|
<SuggestedArtists />
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,59 +1,65 @@
|
|||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||||
import { ItemCard } from '../../../components/Global/components/item-card'
|
import { ItemCard } from '../../../components/Global/components/item-card'
|
||||||
import { useDiscoverContext } from '../../../providers/Discover'
|
|
||||||
import { H5, View, XStack } from 'tamagui'
|
import { H5, View, XStack } from 'tamagui'
|
||||||
import { H4 } from '../../../components/Global/helpers/text'
|
|
||||||
import Icon from '../../Global/components/icon'
|
import Icon from '../../Global/components/icon'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import DiscoverStackParamList from '../../../screens/Discover/types'
|
import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||||
import navigationRef from '../../../../navigation'
|
import navigationRef from '../../../../navigation'
|
||||||
import { useRecentlyAddedAlbums } from '../../../api/queries/album'
|
import { useRecentlyAddedAlbums } from '../../../api/queries/album'
|
||||||
|
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||||
|
|
||||||
export default function RecentlyAdded(): React.JSX.Element {
|
export default function RecentlyAdded(): React.JSX.Element | undefined {
|
||||||
const recentlyAddedAlbumsInfinityQuery = useRecentlyAddedAlbums()
|
const recentlyAddedAlbumsInfinityQuery = useRecentlyAddedAlbums()
|
||||||
|
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
recentlyAddedAlbumsInfinityQuery.data && (
|
||||||
<XStack
|
<Animated.View
|
||||||
alignItems='center'
|
entering={FadeIn.springify()}
|
||||||
onPress={() => {
|
exiting={FadeOut.springify()}
|
||||||
navigation.navigate('RecentlyAdded', {
|
layout={LinearTransition.springify()}
|
||||||
albumsInfiniteQuery: recentlyAddedAlbumsInfinityQuery,
|
testID='discover-recently-added'
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<H5 marginLeft={'$2'}>Recently Added</H5>
|
<XStack
|
||||||
<Icon name='arrow-right' />
|
alignItems='center'
|
||||||
</XStack>
|
onPress={() => {
|
||||||
|
navigation.navigate('RecentlyAdded', {
|
||||||
|
albumsInfiniteQuery: recentlyAddedAlbumsInfinityQuery,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<H5 marginLeft={'$2'}>Recently Added</H5>
|
||||||
|
<Icon name='arrow-right' />
|
||||||
|
</XStack>
|
||||||
|
|
||||||
<HorizontalCardList
|
<HorizontalCardList
|
||||||
data={recentlyAddedAlbumsInfinityQuery.data?.slice(0, 10) ?? []}
|
data={recentlyAddedAlbumsInfinityQuery.data?.slice(0, 10) ?? []}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<ItemCard
|
<ItemCard
|
||||||
caption={item.Name}
|
caption={item.Name}
|
||||||
subCaption={`${item.Artists?.join(', ')}`}
|
subCaption={`${item.Artists?.join(', ')}`}
|
||||||
squared
|
squared
|
||||||
size={'$11'}
|
size={'$11'}
|
||||||
item={item}
|
item={item}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Album', {
|
navigation.navigate('Album', {
|
||||||
album: item,
|
album: item,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onLongPress={() => {
|
onLongPress={() => {
|
||||||
navigationRef.navigate('Context', {
|
navigationRef.navigate('Context', {
|
||||||
item,
|
item,
|
||||||
navigation,
|
navigation,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
gap={'$1'}
|
gap={'$1'}
|
||||||
captionAlign='left'
|
captionAlign='left'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</Animated.View>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { H5, View, XStack } from 'tamagui'
|
import { H5, XStack } from 'tamagui'
|
||||||
import { useDiscoverContext } from '../../../providers/Discover'
|
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
import Icon from '../../Global/components/icon'
|
import Icon from '../../Global/components/icon'
|
||||||
import HorizontalCardList from '../../Global/components/horizontal-list'
|
import HorizontalCardList from '../../Global/components/horizontal-list'
|
||||||
@@ -9,65 +8,74 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import DiscoverStackParamList from '../../../screens/Discover/types'
|
import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||||
import navigationRef from '../../../../navigation'
|
import navigationRef from '../../../../navigation'
|
||||||
import { useJellifyServer } from '../../../stores'
|
import { useJellifyServer } from '../../../stores'
|
||||||
|
import { usePublicPlaylists } from '../../../api/queries/playlist'
|
||||||
|
import Animated, { FadeIn, LinearTransition } from 'react-native-reanimated'
|
||||||
|
|
||||||
export default function PublicPlaylists() {
|
export default function PublicPlaylists() {
|
||||||
const {
|
const {
|
||||||
publicPlaylists,
|
data: playlists,
|
||||||
fetchNextPublicPlaylists,
|
fetchNextPage,
|
||||||
hasNextPublicPlaylists,
|
hasNextPage,
|
||||||
isFetchingNextPublicPlaylists,
|
isPending,
|
||||||
isPendingPublicPlaylists,
|
isFetchingNextPage,
|
||||||
refetchPublicPlaylists,
|
refetch,
|
||||||
} = useDiscoverContext()
|
} = usePublicPlaylists()
|
||||||
|
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
||||||
|
|
||||||
const [server] = useJellifyServer()
|
const [server] = useJellifyServer()
|
||||||
const { width } = useSafeAreaFrame()
|
const { width } = useSafeAreaFrame()
|
||||||
return (
|
return (
|
||||||
<View>
|
playlists && (
|
||||||
<XStack
|
<Animated.View
|
||||||
alignItems='center'
|
entering={FadeIn.springify()}
|
||||||
onPress={() => {
|
exiting={FadeIn.springify()}
|
||||||
navigation.navigate('PublicPlaylists', {
|
layout={LinearTransition.springify()}
|
||||||
playlists: publicPlaylists,
|
testID='discover-public-playlists'
|
||||||
navigation: navigation,
|
|
||||||
fetchNextPage: fetchNextPublicPlaylists,
|
|
||||||
hasNextPage: hasNextPublicPlaylists,
|
|
||||||
isPending: isPendingPublicPlaylists,
|
|
||||||
isFetchingNextPage: isFetchingNextPublicPlaylists,
|
|
||||||
refetch: refetchPublicPlaylists,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<H5 marginLeft={'$2'} lineBreakStrategyIOS='standard' maxWidth={width * 0.8}>
|
<XStack
|
||||||
Playlists on {server?.name ?? 'Jellyfin'}
|
alignItems='center'
|
||||||
</H5>
|
onPress={() => {
|
||||||
<Icon name='arrow-right' />
|
navigation.navigate('PublicPlaylists', {
|
||||||
</XStack>
|
playlists,
|
||||||
<HorizontalCardList
|
navigation: navigation,
|
||||||
data={publicPlaylists?.slice(0, 10) ?? []}
|
fetchNextPage,
|
||||||
renderItem={({ item }) => (
|
hasNextPage,
|
||||||
<ItemCard
|
isPending,
|
||||||
caption={item.Name}
|
isFetchingNextPage,
|
||||||
subCaption={`${item.Genres?.join(', ')}`}
|
refetch,
|
||||||
squared
|
})
|
||||||
size={'$10'}
|
}}
|
||||||
item={item}
|
>
|
||||||
onPress={() => {
|
<H5 marginLeft={'$2'} lineBreakStrategyIOS='standard' maxWidth={width * 0.8}>
|
||||||
navigation.navigate('Playlist', { playlist: item, canEdit: false })
|
Playlists on {server?.name ?? 'Jellyfin'}
|
||||||
}}
|
</H5>
|
||||||
onLongPress={() =>
|
<Icon name='arrow-right' />
|
||||||
navigationRef.navigate('Context', {
|
</XStack>
|
||||||
item,
|
<HorizontalCardList
|
||||||
navigation,
|
data={playlists?.slice(0, 10) ?? []}
|
||||||
})
|
renderItem={({ item }) => (
|
||||||
}
|
<ItemCard
|
||||||
marginHorizontal={'$1'}
|
caption={item.Name}
|
||||||
captionAlign='left'
|
subCaption={`${item.Genres?.join(', ')}`}
|
||||||
/>
|
squared
|
||||||
)}
|
size={'$10'}
|
||||||
/>
|
item={item}
|
||||||
</View>
|
onPress={() => {
|
||||||
|
navigation.navigate('Playlist', { playlist: item, canEdit: false })
|
||||||
|
}}
|
||||||
|
onLongPress={() =>
|
||||||
|
navigationRef.navigate('Context', {
|
||||||
|
item,
|
||||||
|
navigation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
marginHorizontal={'$1'}
|
||||||
|
captionAlign='left'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,54 +3,62 @@ import Icon from '../../Global/components/icon'
|
|||||||
import HorizontalCardList from '../../Global/components/horizontal-list'
|
import HorizontalCardList from '../../Global/components/horizontal-list'
|
||||||
import { ItemCard } from '../../Global/components/item-card'
|
import { ItemCard } from '../../Global/components/item-card'
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||||
import { useDiscoverContext } from '../../../providers/Discover'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import DiscoverStackParamList from '../../../screens/Discover/types'
|
import DiscoverStackParamList from '../../../screens/Discover/types'
|
||||||
import navigationRef from '../../../../navigation'
|
import navigationRef from '../../../../navigation'
|
||||||
import { pickFirstGenre } from '../../../utils/genre-formatting'
|
import { pickFirstGenre } from '../../../utils/genre-formatting'
|
||||||
|
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||||
|
import { useDiscoverArtists } from '../../../api/queries/suggestions'
|
||||||
|
|
||||||
export default function SuggestedArtists(): React.JSX.Element {
|
export default function SuggestedArtists(): React.JSX.Element | undefined {
|
||||||
const { suggestedArtistsInfiniteQuery } = useDiscoverContext()
|
const suggestedArtistsInfiniteQuery = useDiscoverArtists()
|
||||||
|
|
||||||
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
suggestedArtistsInfiniteQuery.data && (
|
||||||
<XStack
|
<Animated.View
|
||||||
alignItems='center'
|
entering={FadeIn.springify()}
|
||||||
onPress={() => {
|
exiting={FadeOut.springify()}
|
||||||
navigation.navigate('SuggestedArtists', {
|
layout={LinearTransition.springify()}
|
||||||
artistsInfiniteQuery: suggestedArtistsInfiniteQuery,
|
testID='discover-suggested-artists'
|
||||||
navigation: navigation,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
marginLeft={'$2'}
|
|
||||||
>
|
>
|
||||||
<H5>Suggested Artists</H5>
|
<XStack
|
||||||
<Icon name='arrow-right' />
|
alignItems='center'
|
||||||
</XStack>
|
onPress={() => {
|
||||||
<HorizontalCardList
|
navigation.navigate('SuggestedArtists', {
|
||||||
data={suggestedArtistsInfiniteQuery.data?.slice(0, 10) ?? []}
|
artistsInfiniteQuery: suggestedArtistsInfiniteQuery,
|
||||||
renderItem={({ item }) => (
|
navigation: navigation,
|
||||||
<ItemCard
|
})
|
||||||
caption={item.Name}
|
}}
|
||||||
subCaption={pickFirstGenre(item.Genres)}
|
marginLeft={'$2'}
|
||||||
size={'$10'}
|
>
|
||||||
item={item}
|
<H5>Suggested Artists</H5>
|
||||||
onPress={() => {
|
<Icon name='arrow-right' />
|
||||||
navigation.navigate('Artist', {
|
</XStack>
|
||||||
artist: item,
|
<HorizontalCardList
|
||||||
})
|
data={suggestedArtistsInfiniteQuery.data?.slice(0, 10) ?? []}
|
||||||
}}
|
renderItem={({ item }) => (
|
||||||
onLongPress={() =>
|
<ItemCard
|
||||||
navigationRef.navigate('Context', {
|
caption={item.Name}
|
||||||
item,
|
subCaption={pickFirstGenre(item.Genres)}
|
||||||
navigation,
|
size={'$10'}
|
||||||
})
|
item={item}
|
||||||
}
|
onPress={() => {
|
||||||
/>
|
navigation.navigate('Artist', {
|
||||||
)}
|
artist: item,
|
||||||
/>
|
})
|
||||||
</View>
|
}}
|
||||||
|
onLongPress={() =>
|
||||||
|
navigationRef.navigate('Context', {
|
||||||
|
item,
|
||||||
|
navigation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { QueryKeys } from '../../enums/query-keys'
|
|||||||
import { fetchSearchResults } from '../../api/queries/search'
|
import { fetchSearchResults } from '../../api/queries/search'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { FlatList } from 'react-native'
|
import { FlatList } from 'react-native'
|
||||||
import { fetchSearchSuggestions } from '../../api/queries/suggestions'
|
import { fetchSearchSuggestions } from '../../api/queries/suggestions/utils/suggestions'
|
||||||
import { getToken, H3, Separator, Spinner, YStack } from 'tamagui'
|
import { getToken, H3, Separator, Spinner, YStack } from 'tamagui'
|
||||||
import Suggestions from './suggestions'
|
import Suggestions from './suggestions'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
@@ -15,6 +15,7 @@ import { ItemCard } from '../Global/components/item-card'
|
|||||||
import SearchParamList from '../../screens/Search/types'
|
import SearchParamList from '../../screens/Search/types'
|
||||||
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
||||||
import { useApi, useJellifyLibrary, useJellifyUser } from '../../stores'
|
import { useApi, useJellifyLibrary, useJellifyUser } from '../../stores'
|
||||||
|
import { useSearchSuggestions } from '../../api/queries/suggestions'
|
||||||
|
|
||||||
export default function Search({
|
export default function Search({
|
||||||
navigation,
|
navigation,
|
||||||
@@ -40,10 +41,7 @@ export default function Search({
|
|||||||
data: suggestions,
|
data: suggestions,
|
||||||
isFetching: fetchingSuggestions,
|
isFetching: fetchingSuggestions,
|
||||||
refetch: refetchSuggestions,
|
refetch: refetchSuggestions,
|
||||||
} = useQuery({
|
} = useSearchSuggestions()
|
||||||
queryKey: [QueryKeys.SearchSuggestions, library?.musicLibraryId],
|
|
||||||
queryFn: () => fetchSearchSuggestions(api, user, library?.musicLibraryId),
|
|
||||||
})
|
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
let timeout: ReturnType<typeof setTimeout>
|
let timeout: ReturnType<typeof setTimeout>
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import {
|
|
||||||
InfiniteQueryObserverResult,
|
|
||||||
useInfiniteQuery,
|
|
||||||
UseInfiniteQueryResult,
|
|
||||||
} from '@tanstack/react-query'
|
|
||||||
import { QueryKeys } from '../../enums/query-keys'
|
|
||||||
import { createContext, ReactNode, useContext, useState } from 'react'
|
|
||||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
|
||||||
import { fetchPublicPlaylists } from '../../api/queries/playlist/utils'
|
|
||||||
import { fetchArtistSuggestions } from '../../api/queries/suggestions'
|
|
||||||
import { useRefetchRecentlyAdded } from '../../api/queries/album'
|
|
||||||
import { useApi, useJellifyUser, useJellifyLibrary } from '../../stores'
|
|
||||||
|
|
||||||
interface DiscoverContext {
|
|
||||||
refreshing: boolean
|
|
||||||
refresh: () => void
|
|
||||||
publicPlaylists: BaseItemDto[] | undefined
|
|
||||||
fetchNextPublicPlaylists: () => void
|
|
||||||
hasNextPublicPlaylists: boolean
|
|
||||||
isPendingPublicPlaylists: boolean
|
|
||||||
isFetchingNextPublicPlaylists: boolean
|
|
||||||
refetchPublicPlaylists: () => void
|
|
||||||
suggestedArtistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
|
|
||||||
}
|
|
||||||
|
|
||||||
const DiscoverContextInitializer = () => {
|
|
||||||
const api = useApi()
|
|
||||||
const [user] = useJellifyUser()
|
|
||||||
const [library] = useJellifyLibrary()
|
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const refetchRecentlyAdded = useRefetchRecentlyAdded()
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: publicPlaylists,
|
|
||||||
refetch: refetchPublicPlaylists,
|
|
||||||
fetchNextPage: fetchNextPublicPlaylists,
|
|
||||||
hasNextPage: hasNextPublicPlaylists,
|
|
||||||
isPending: isPendingPublicPlaylists,
|
|
||||||
isFetchingNextPage: isFetchingNextPublicPlaylists,
|
|
||||||
} = useInfiniteQuery({
|
|
||||||
queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId],
|
|
||||||
queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam),
|
|
||||||
select: (data) => data.pages.flatMap((page) => page),
|
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
|
||||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
|
||||||
initialPageParam: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const suggestedArtistsInfiniteQuery = useInfiniteQuery({
|
|
||||||
queryKey: [QueryKeys.InfiniteSuggestedArtists, user?.id, library?.musicLibraryId],
|
|
||||||
queryFn: ({ pageParam }) =>
|
|
||||||
fetchArtistSuggestions(api, user, library?.musicLibraryId, pageParam),
|
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
|
||||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
|
||||||
select: (data) => data.pages.flatMap((page) => page),
|
|
||||||
initialPageParam: 0,
|
|
||||||
maxPages: 2,
|
|
||||||
})
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
setRefreshing(true)
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
refetchRecentlyAdded(),
|
|
||||||
refetchPublicPlaylists(),
|
|
||||||
suggestedArtistsInfiniteQuery.refetch(),
|
|
||||||
])
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
refreshing,
|
|
||||||
refresh,
|
|
||||||
publicPlaylists,
|
|
||||||
fetchNextPublicPlaylists,
|
|
||||||
hasNextPublicPlaylists,
|
|
||||||
isPendingPublicPlaylists,
|
|
||||||
isFetchingNextPublicPlaylists,
|
|
||||||
refetchPublicPlaylists,
|
|
||||||
suggestedArtistsInfiniteQuery,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DiscoverContext = createContext<DiscoverContext>({
|
|
||||||
refreshing: false,
|
|
||||||
refresh: () => {},
|
|
||||||
publicPlaylists: undefined,
|
|
||||||
fetchNextPublicPlaylists: () => {},
|
|
||||||
hasNextPublicPlaylists: false,
|
|
||||||
isPendingPublicPlaylists: false,
|
|
||||||
isFetchingNextPublicPlaylists: false,
|
|
||||||
refetchPublicPlaylists: () => {},
|
|
||||||
suggestedArtistsInfiniteQuery: {
|
|
||||||
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<BaseItemDto[], Error>),
|
|
||||||
fetchNextPage: async () =>
|
|
||||||
Promise.resolve({} as InfiniteQueryObserverResult<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<BaseItemDto[], Error>),
|
|
||||||
promise: Promise.resolve([]),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode
|
|
||||||
}) => {
|
|
||||||
const context = DiscoverContextInitializer()
|
|
||||||
|
|
||||||
return <DiscoverContext.Provider value={context}>{children}</DiscoverContext.Provider>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDiscoverContext = () => useContext(DiscoverContext)
|
|
||||||
@@ -2,7 +2,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
|||||||
import Index from '../../components/Discover/component'
|
import Index from '../../components/Discover/component'
|
||||||
import AlbumScreen from '../Album'
|
import AlbumScreen from '../Album'
|
||||||
import { ArtistScreen } from '../Artist'
|
import { ArtistScreen } from '../Artist'
|
||||||
import { DiscoverProvider } from '../../providers/Discover'
|
|
||||||
import { useTheme } from 'tamagui'
|
import { useTheme } from 'tamagui'
|
||||||
import RecentlyAdded from './albums'
|
import RecentlyAdded from './albums'
|
||||||
import PublicPlaylists from './playlists'
|
import PublicPlaylists from './playlists'
|
||||||
@@ -18,87 +17,85 @@ export function Discover(): React.JSX.Element {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiscoverProvider>
|
<DiscoverStack.Navigator initialRouteName='Discover'>
|
||||||
<DiscoverStack.Navigator initialRouteName='Discover'>
|
<DiscoverStack.Screen
|
||||||
<DiscoverStack.Screen
|
name='Discover'
|
||||||
name='Discover'
|
component={Index}
|
||||||
component={Index}
|
options={{
|
||||||
options={{
|
headerTitleStyle: {
|
||||||
headerTitleStyle: {
|
fontFamily: 'Figtree-Bold',
|
||||||
fontFamily: 'Figtree-Bold',
|
},
|
||||||
},
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<DiscoverStack.Screen
|
<DiscoverStack.Screen
|
||||||
name='Artist'
|
name='Artist'
|
||||||
component={ArtistScreen}
|
component={ArtistScreen}
|
||||||
options={({ route }) => ({
|
options={({ route }) => ({
|
||||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
color: theme.background.val,
|
color: theme.background.val,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiscoverStack.Screen
|
<DiscoverStack.Screen
|
||||||
name='Album'
|
name='Album'
|
||||||
component={AlbumScreen}
|
component={AlbumScreen}
|
||||||
options={({ route }) => ({
|
options={({ route }) => ({
|
||||||
title: route.params.album.Name ?? 'Untitled Album',
|
title: route.params.album.Name ?? 'Untitled Album',
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
color: theme.background.val,
|
color: theme.background.val,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiscoverStack.Screen
|
<DiscoverStack.Screen
|
||||||
name='Playlist'
|
name='Playlist'
|
||||||
component={PlaylistScreen}
|
component={PlaylistScreen}
|
||||||
options={({ route }) => ({
|
options={({ route }) => ({
|
||||||
title: route.params.playlist.Name ?? 'Untitled Playlist',
|
title: route.params.playlist.Name ?? 'Untitled Playlist',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiscoverStack.Screen
|
<DiscoverStack.Screen
|
||||||
name='RecentlyAdded'
|
name='RecentlyAdded'
|
||||||
component={RecentlyAdded}
|
component={RecentlyAdded}
|
||||||
options={{
|
options={{
|
||||||
title: 'Recently Added',
|
title: 'Recently Added',
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
fontFamily: 'Figtree-Bold',
|
fontFamily: 'Figtree-Bold',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiscoverStack.Screen
|
<DiscoverStack.Screen
|
||||||
name='PublicPlaylists'
|
name='PublicPlaylists'
|
||||||
component={PublicPlaylists}
|
component={PublicPlaylists}
|
||||||
options={{
|
options={{
|
||||||
title: 'Public Playlists',
|
title: 'Public Playlists',
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
fontFamily: 'Figtree-Bold',
|
fontFamily: 'Figtree-Bold',
|
||||||
color: theme.background.val,
|
color: theme.background.val,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiscoverStack.Screen
|
<DiscoverStack.Screen
|
||||||
name='SuggestedArtists'
|
name='SuggestedArtists'
|
||||||
component={SuggestedArtists}
|
component={SuggestedArtists}
|
||||||
options={{
|
options={{
|
||||||
title: 'Suggested Artists',
|
title: 'Suggested Artists',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiscoverStack.Screen
|
<DiscoverStack.Screen
|
||||||
name='InstantMix'
|
name='InstantMix'
|
||||||
component={InstantMix}
|
component={InstantMix}
|
||||||
options={({ route }) => ({
|
options={({ route }) => ({
|
||||||
headerTitle: `${getItemName(route.params.item)} Mix`,
|
headerTitle: `${getItemName(route.params.item)} Mix`,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</DiscoverStack.Navigator>
|
</DiscoverStack.Navigator>
|
||||||
</DiscoverProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user