mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-03 01:20:10 -06:00
612 feature display activity indicator when favoriting (#629)
Making adding favorites far smoother, displaying loading simulators when operations are underway
This commit is contained in:
104
src/api/mutations/favorite/index.ts
Normal file
104
src/api/mutations/favorite/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { isUndefined } from 'lodash'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import UserDataQueryKey from '../../queries/user-data/keys'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto
|
||||
onToggle?: () => void
|
||||
}
|
||||
|
||||
export const useAddFavorite = () => {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ item }: SetFavoriteMutation) => {
|
||||
if (isUndefined(api)) Promise.reject('API instance not defined')
|
||||
else if (isUndefined(item.Id)) Promise.reject('Item ID is undefined')
|
||||
else
|
||||
return await getUserLibraryApi(api).markFavoriteItem({
|
||||
itemId: item.Id,
|
||||
})
|
||||
},
|
||||
onSuccess: (data, { item, onToggle }) => {
|
||||
Toast.show({
|
||||
text1: 'Added favorite',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
trigger('notificationSuccess')
|
||||
|
||||
if (onToggle) onToggle()
|
||||
|
||||
if (user)
|
||||
queryClient.setQueryData(UserDataQueryKey(user, item), (prev: UserItemDataDto) => {
|
||||
return {
|
||||
...prev,
|
||||
IsFavorite: true,
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: (error, variables) => {
|
||||
console.error('Unable to set favorite for item', error)
|
||||
|
||||
trigger('notificationError')
|
||||
|
||||
Toast.show({
|
||||
text1: 'Failed to add favorite',
|
||||
type: 'error',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveFavorite = () => {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ item }: SetFavoriteMutation) => {
|
||||
if (isUndefined(api)) Promise.reject('API instance not defined')
|
||||
else if (isUndefined(item.Id)) Promise.reject('Item ID is undefined')
|
||||
else
|
||||
return await getUserLibraryApi(api).unmarkFavoriteItem({
|
||||
itemId: item.Id,
|
||||
})
|
||||
},
|
||||
onSuccess: (data, { item, onToggle }) => {
|
||||
Toast.show({
|
||||
text1: 'Removed favorite',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
trigger('notificationSuccess')
|
||||
|
||||
if (onToggle) onToggle()
|
||||
|
||||
if (user)
|
||||
queryClient.setQueryData(UserDataQueryKey(user, item), (prev: UserItemDataDto) => {
|
||||
return {
|
||||
...prev,
|
||||
IsFavorite: false,
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: (error, variables) => {
|
||||
console.error('Unable to remove favorite for item', error)
|
||||
|
||||
trigger('notificationError')
|
||||
|
||||
Toast.show({
|
||||
text1: 'Failed to remove favorite',
|
||||
type: 'error',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -32,6 +32,7 @@ const useStreamedMediaInfo = (itemId: string | null | undefined) => {
|
||||
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
|
||||
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
|
||||
staleTime: Infinity, // Only refetch when the user's device profile changes
|
||||
gcTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,5 +61,6 @@ export const useDownloadedMediaInfo = (itemId: string | null | undefined) => {
|
||||
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
|
||||
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
|
||||
staleTime: Infinity, // Only refetch when the user's device profile changes
|
||||
gcTime: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import Icon from './icon'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useJellifyUserDataContext } from '../../../providers/UserData'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { useAddFavorite, useRemoveFavorite } from '../../../api/mutations/favorite'
|
||||
import { useIsFavorite } from '../../../api/queries/user-data'
|
||||
import { getTokenValue, Spinner } from 'tamagui'
|
||||
|
||||
interface FavoriteButtonProps {
|
||||
item: BaseItemDto
|
||||
@@ -12,30 +12,29 @@ interface FavoriteButtonProps {
|
||||
}
|
||||
|
||||
export default function FavoriteButton({ item, onToggle }: FavoriteButtonProps): React.JSX.Element {
|
||||
const { toggleFavorite } = useJellifyUserDataContext()
|
||||
const { data: isFavorite, isPending } = useIsFavorite(item)
|
||||
|
||||
const { data: isFavorite } = useIsFavorite(item)
|
||||
return isPending ? (
|
||||
<Spinner color={'$primary'} width={34 + getTokenValue('$0.5')} height={'$1'} />
|
||||
) : isFavorite ? (
|
||||
<AddFavoriteButton item={item} onToggle={onToggle} />
|
||||
) : (
|
||||
<RemoveFavoriteButton item={item} onToggle={onToggle} />
|
||||
)
|
||||
}
|
||||
|
||||
return isFavorite ? (
|
||||
function AddFavoriteButton({ item, onToggle }: FavoriteButtonProps): React.JSX.Element {
|
||||
const { mutate, isPending } = useRemoveFavorite()
|
||||
|
||||
return isPending ? (
|
||||
<Spinner color={'$primary'} width={34 + getTokenValue('$0.5')} height={'$1'} />
|
||||
) : (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
<Icon
|
||||
name={'heart'}
|
||||
color={'$primary'}
|
||||
onPress={() =>
|
||||
toggleFavorite(isFavorite, {
|
||||
item,
|
||||
onToggle,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
<Icon
|
||||
name={'heart-outline'}
|
||||
color={'$primary'}
|
||||
onPress={() =>
|
||||
toggleFavorite(!!isFavorite, {
|
||||
mutate({
|
||||
item,
|
||||
onToggle,
|
||||
})
|
||||
@@ -45,10 +44,23 @@ export default function FavoriteButton({ item, onToggle }: FavoriteButtonProps):
|
||||
)
|
||||
}
|
||||
|
||||
export function isFavoriteItem(item: BaseItemDto): boolean {
|
||||
return isUndefined(item.UserData)
|
||||
? false
|
||||
: isUndefined(item.UserData.IsFavorite)
|
||||
? false
|
||||
: item.UserData.IsFavorite
|
||||
function RemoveFavoriteButton({ item, onToggle }: FavoriteButtonProps): React.JSX.Element {
|
||||
const { mutate, isPending } = useAddFavorite()
|
||||
|
||||
return isPending ? (
|
||||
<Spinner color={'$primary'} width={34 + getTokenValue('$0.5')} height={'$1'} />
|
||||
) : (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
<Icon
|
||||
name={'heart-outline'}
|
||||
color={'$primary'}
|
||||
onPress={() =>
|
||||
mutate({
|
||||
item,
|
||||
onToggle,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,34 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { ListItem, XStack } from 'tamagui'
|
||||
import Icon from './icon'
|
||||
import { useJellifyUserDataContext } from '../../../providers/UserData'
|
||||
import { Text } from '../helpers/text'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { useIsFavorite } from '../../../api/queries/user-data'
|
||||
import { useAddFavorite, useRemoveFavorite } from '../../../api/mutations/favorite'
|
||||
|
||||
export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const { toggleFavorite } = useJellifyUserDataContext()
|
||||
|
||||
const { data: isFavorite, refetch } = useIsFavorite(item)
|
||||
const { data: isFavorite } = useIsFavorite(item)
|
||||
|
||||
return isFavorite ? (
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
backgroundColor={'transparent'}
|
||||
justifyContent='flex-start'
|
||||
onPress={() => {
|
||||
toggleFavorite(isFavorite, {
|
||||
item,
|
||||
onToggle: () => refetch(),
|
||||
})
|
||||
}}
|
||||
pressStyle={{ opacity: 0.5 }}
|
||||
>
|
||||
<Animated.View
|
||||
entering={FadeIn}
|
||||
exiting={FadeOut}
|
||||
key={`${item.Id}-remove-favorite-row`}
|
||||
>
|
||||
<XStack alignItems='center' justifyContent='flex-start' gap={'$2.5'}>
|
||||
<Icon name={'heart'} small color={'$primary'} />
|
||||
|
||||
<Text bold>Remove from favorites</Text>
|
||||
</XStack>
|
||||
</Animated.View>
|
||||
</ListItem>
|
||||
<RemoveFavoriteContextMenuRow item={item} />
|
||||
) : (
|
||||
<AddFavoriteContextMenuRow item={item} />
|
||||
)
|
||||
}
|
||||
|
||||
function AddFavoriteContextMenuRow({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const { mutate, isPending } = useAddFavorite()
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
backgroundColor={'transparent'}
|
||||
justifyContent='flex-start'
|
||||
onPress={() => {
|
||||
toggleFavorite(!!isFavorite, {
|
||||
item,
|
||||
onToggle: () => refetch(),
|
||||
})
|
||||
mutate({ item })
|
||||
}}
|
||||
pressStyle={{ opacity: 0.5 }}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut} key={`${item.Id}-favorite-row`}>
|
||||
<XStack alignItems='center' justifyContent='flex-start' gap={'$2.5'}>
|
||||
@@ -59,3 +40,28 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }):
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
function RemoveFavoriteContextMenuRow({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const { mutate, isPending } = useRemoveFavorite()
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
backgroundColor={'transparent'}
|
||||
justifyContent='flex-start'
|
||||
onPress={() => {
|
||||
mutate({ item })
|
||||
}}
|
||||
pressStyle={{ opacity: 0.5 }}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut} key={`${item.Id}-favorite-row`}>
|
||||
<XStack alignItems='center' justifyContent='flex-start' gap={'$2.5'}>
|
||||
<Icon small name={'heart'} color={'$primary'} />
|
||||
|
||||
<Text bold>Remove from favorites</Text>
|
||||
</XStack>
|
||||
</Animated.View>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { useEffect } from 'react'
|
||||
import Root from '../screens'
|
||||
import { PlayerProvider } from '../providers/Player'
|
||||
import { JellifyProvider, useJellifyContext } from '../providers'
|
||||
import { JellifyUserDataProvider } from '../providers/UserData'
|
||||
import { NetworkContextProvider } from '../providers/Network'
|
||||
import { DisplayProvider } from '../providers/Display/display-provider'
|
||||
import {
|
||||
@@ -80,14 +79,11 @@ function App(): React.JSX.Element {
|
||||
}, [sendMetrics])
|
||||
|
||||
return (
|
||||
<JellifyUserDataProvider>
|
||||
<NetworkContextProvider>
|
||||
<PlayerProvider />
|
||||
<CarPlayProvider />
|
||||
<Root />
|
||||
</NetworkContextProvider>
|
||||
|
||||
<NetworkContextProvider>
|
||||
<PlayerProvider />
|
||||
<CarPlayProvider />
|
||||
<Root />
|
||||
<Toast topOffset={getToken('$12')} config={JellifyToastConfig(theme)} />
|
||||
</JellifyUserDataProvider>
|
||||
</NetworkContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { createContext, ReactNode, useContext } from 'react'
|
||||
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useJellifyContext } from '..'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
import UserDataQueryKey from '../../api/queries/user-data/keys'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto
|
||||
onToggle?: () => void
|
||||
}
|
||||
|
||||
interface JellifyUserDataContext {
|
||||
toggleFavorite: (isFavorite: boolean, mutation: SetFavoriteMutation) => void
|
||||
}
|
||||
|
||||
const JellifyUserDataContextInitializer = () => {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const useSetFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(api!).markFavoriteItem({
|
||||
itemId: mutation.item.Id!,
|
||||
})
|
||||
},
|
||||
onSuccess: ({ data }, { item, onToggle }) => {
|
||||
// Burnt.alert({
|
||||
// title: `Added favorite`,
|
||||
// duration: 1,
|
||||
// preset: 'heart',
|
||||
// })
|
||||
Toast.show({
|
||||
text1: 'Added favorite',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
trigger('notificationSuccess')
|
||||
|
||||
if (onToggle) onToggle()
|
||||
|
||||
// Force refresh of track user data
|
||||
queryClient.invalidateQueries({ queryKey: UserDataQueryKey(user!, item) })
|
||||
},
|
||||
})
|
||||
|
||||
const useRemoveFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(api!).unmarkFavoriteItem({
|
||||
itemId: mutation.item.Id!,
|
||||
})
|
||||
},
|
||||
onSuccess: ({ data }, { item, onToggle }) => {
|
||||
// Burnt.alert({
|
||||
// title: `Removed favorite`,
|
||||
// duration: 1,
|
||||
// preset: 'done',
|
||||
// })
|
||||
Toast.show({
|
||||
text1: 'Removed favorite',
|
||||
type: 'error',
|
||||
})
|
||||
trigger('notificationSuccess')
|
||||
|
||||
if (onToggle) onToggle()
|
||||
|
||||
// Force refresh of track user data
|
||||
queryClient.invalidateQueries({ queryKey: UserDataQueryKey(user!, item) })
|
||||
},
|
||||
})
|
||||
|
||||
const toggleFavorite = (isFavorite: boolean, mutation: SetFavoriteMutation) =>
|
||||
(isFavorite ? useRemoveFavorite : useSetFavorite).mutate(mutation)
|
||||
|
||||
return {
|
||||
toggleFavorite,
|
||||
}
|
||||
}
|
||||
|
||||
const JellifyUserDataContext = createContext<JellifyUserDataContext>({
|
||||
toggleFavorite: () => {},
|
||||
})
|
||||
|
||||
export const JellifyUserDataProvider: ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
const { toggleFavorite } = JellifyUserDataContextInitializer()
|
||||
|
||||
return (
|
||||
<JellifyUserDataContext.Provider
|
||||
value={{
|
||||
toggleFavorite,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</JellifyUserDataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useJellifyUserDataContext = () => useContext(JellifyUserDataContext)
|
||||
Reference in New Issue
Block a user