Fix/tamagui switch refactor down stream (#955)

* Refactor haptic feedback handling in SwitchWithLabel component
This commit is contained in:
skalthoff
2026-01-26 21:21:39 -08:00
committed by GitHub
parent 21b5e0199a
commit 3752f40d47
16 changed files with 51 additions and 113 deletions
+5 -9
View File
@@ -1,5 +1,5 @@
import { queryClient } from '../../../constants/query-client'
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
import { BaseItemDto, BaseItemKind, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
import { useMutation } from '@tanstack/react-query'
@@ -73,8 +73,6 @@ function invalidateRelevantQueries(item: BaseItemDto): void {
}
export const useAddFavorite = () => {
const trigger = useHapticFeedback()
return useMutation({
mutationFn: async ({ item }: SetFavoriteMutation) => {
const api = getApi()
@@ -87,7 +85,7 @@ export const useAddFavorite = () => {
})
},
onSuccess: (data, { item, onToggle }) => {
trigger('notificationSuccess')
triggerHaptic('notificationSuccess')
const user = getUser()
@@ -107,7 +105,7 @@ export const useAddFavorite = () => {
onError: (error, variables) => {
console.error('Unable to set favorite for item', error)
trigger('notificationError')
triggerHaptic('notificationError')
Toast.show({
text1: 'Failed to add favorite',
@@ -118,8 +116,6 @@ export const useAddFavorite = () => {
}
export const useRemoveFavorite = () => {
const trigger = useHapticFeedback()
return useMutation({
mutationFn: async ({ item }: SetFavoriteMutation) => {
const api = getApi()
@@ -132,7 +128,7 @@ export const useRemoveFavorite = () => {
})
},
onSuccess: (data, { item, onToggle }) => {
trigger('notificationSuccess')
triggerHaptic('notificationSuccess')
const user = getUser()
@@ -152,7 +148,7 @@ export const useRemoveFavorite = () => {
onError: (error, variables) => {
console.error('Unable to remove favorite for item', error)
trigger('notificationError')
triggerHaptic('notificationError')
Toast.show({
text1: 'Failed to remove favorite',
+4 -6
View File
@@ -9,7 +9,7 @@ import ItemImage from '../Global/components/image'
import TextTicker from 'react-native-text-ticker'
import { TextTickerConfig } from '../Player/component.config'
import { getItemName } from '../../utils/formatting/item-names'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { usePlaylistTracks, useUserPlaylists } from '../../api/queries/playlist'
import { getApi, getUser } from '../../stores'
import Animated, { Easing, FadeIn, FadeOut } from 'react-native-reanimated'
@@ -94,8 +94,6 @@ function AddToPlaylistRow({
tracks: BaseItemDto[]
visible: boolean
}): React.JSX.Element {
const trigger = useHapticFeedback()
const { data: playlistTracks, isPending: fetchingPlaylistTracks } = usePlaylistTracks(
playlist,
!visible,
@@ -103,7 +101,7 @@ function AddToPlaylistRow({
const useAddToPlaylist = useMutation({
mutationFn: ({ playlist, tracks }: AddToPlaylistMutation) => {
trigger('impactLight')
triggerHaptic('impactLight')
const api = getApi()
const user = getUser()
@@ -111,7 +109,7 @@ function AddToPlaylistRow({
return addManyToPlaylist(api, user, tracks, playlist)
},
onSuccess: (_, { tracks }) => {
trigger('notificationSuccess')
triggerHaptic('notificationSuccess')
queryClient.setQueryData(
PlaylistTracksQueryKey(playlist),
@@ -129,7 +127,7 @@ function AddToPlaylistRow({
},
onError: (error) => {
console.error(error)
trigger('notificationError')
triggerHaptic('notificationError')
},
})
+2 -4
View File
@@ -26,7 +26,7 @@ import { TextTickerConfig } from '../Player/component.config'
import { useAddToQueue } from '../../hooks/player/callbacks'
import { useIsDownloaded } from '../../api/queries/download'
import { useDeleteDownloads } from '../../api/mutations/download'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { Platform } from 'react-native'
import { useApi } from '../../stores'
import useAddToPendingDownloads, { useIsDownloading } from '../../stores/network/downloads'
@@ -51,8 +51,6 @@ export default function ItemContext({
}: ContextProps): React.JSX.Element {
const api = useApi()
const trigger = useHapticFeedback()
const isArtist = item.Type === BaseItemKind.MusicArtist
const isAlbum = item.Type === BaseItemKind.MusicAlbum
const isTrack = item.Type === BaseItemKind.Audio
@@ -105,7 +103,7 @@ export default function ItemContext({
else return []
})()
useEffect(() => trigger('impactLight'), [item?.Id])
useEffect(() => triggerHaptic('impactLight'), [item?.Id])
return (
<YGroup scrollable={Platform.OS === 'android'} marginBottom={'$8'}>
+3 -5
View File
@@ -3,13 +3,11 @@ import { YStack, XStack } from 'tamagui'
import { Text } from '../Global/helpers/text'
import { CheckboxWithLabel } from '../Global/helpers/checkbox-with-label'
import useLibraryStore from '../../stores/library'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { FiltersProps } from './types'
export default function Filters({ currentTab }: FiltersProps): React.JSX.Element {
const { filters, setTracksFilters, setAlbumsFilters, setArtistsFilters } = useLibraryStore()
const trigger = useHapticFeedback()
if (!currentTab || currentTab === 'Playlists') {
return <></>
}
@@ -19,7 +17,7 @@ export default function Filters({ currentTab }: FiltersProps): React.JSX.Element
const isDownloaded = currentFilters.isDownloaded ?? false
const handleFavoritesToggle = (checked: boolean | 'indeterminate') => {
trigger('impactLight')
triggerHaptic('impactLight')
const newValue = checked === true ? true : undefined
if (currentTab === 'Tracks') {
@@ -32,7 +30,7 @@ export default function Filters({ currentTab }: FiltersProps): React.JSX.Element
}
const handleDownloadedToggle = (checked: boolean | 'indeterminate') => {
trigger('impactLight')
triggerHaptic('impactLight')
if (currentTab === 'Tracks') {
setTracksFilters({ isDownloaded: checked === true })
}
@@ -9,7 +9,7 @@ import Animated, {
cancelAnimation,
} from 'react-native-reanimated'
import Icon from './icon'
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
import {
notifySwipeableRowClosed,
notifySwipeableRowOpened,
@@ -58,7 +58,6 @@ export default function SwipeableRow({
rightActions,
disabled,
}: Props) {
const triggerHaptic = useHapticFeedback()
const tx = useSharedValue(0)
const dragging = useSharedValue(false)
const idRef = useRef<string | undefined>(undefined)
@@ -7,7 +7,7 @@ import { scheduleOnRN } from 'react-native-worklets'
import { Text } from '../helpers/text'
import { UseInfiniteQueryResult, useMutation } from '@tanstack/react-query'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
/**
@@ -26,7 +26,6 @@ export default function AZScroller({
onLetterSelect: (letter: string) => Promise<void>
}) {
const theme = useTheme()
const trigger = useHapticFeedback()
const [operationPending, setOperationPending] = useState<boolean>(false)
@@ -139,7 +138,7 @@ export default function AZScroller({
}
useEffect(() => {
trigger('impactLight')
triggerHaptic('impactLight')
}, [overlayLetter])
return (
+5 -7
View File
@@ -4,7 +4,7 @@ import { Square, XStack, YStack } from 'tamagui'
import Icon from '../Global/components/icon'
import { Text } from '../Global/helpers/text'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import StatusBar from '../Global/helpers/status-bar'
import useLibraryStore from '../../stores/library'
import { handleShuffle } from '../../hooks/player/functions/shuffle'
@@ -13,8 +13,6 @@ import TrackPlayer from 'react-native-track-player'
import navigationRef from '../../../navigation'
function LibraryTabBar(props: MaterialTopTabBarProps) {
const trigger = useHapticFeedback()
const insets = useSafeAreaInsets()
const currentTab = props.state.routes[props.state.index].name as
@@ -34,7 +32,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
(currentFilters.isFavorites === true || currentFilters.isDownloaded === true)
const handleShufflePress = async () => {
trigger('impactLight')
triggerHaptic('impactLight')
// Set queueRef to 'Library' so handleShuffle knows to fetch random tracks
usePlayerQueueStore.getState().setQueueRef('Library')
@@ -69,7 +67,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
{props.state.routes[props.state.index].name === 'Playlists' && (
<XStack
onPress={() => {
trigger('impactLight')
triggerHaptic('impactLight')
props.navigation.navigate('AddPlaylist')
}}
pressStyle={{ opacity: 0.6 }}
@@ -100,7 +98,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
{props.state.routes[props.state.index].name !== 'Playlists' && (
<XStack
onPress={() => {
trigger('impactLight')
triggerHaptic('impactLight')
if (navigationRef.isReady()) {
navigationRef.navigate('Filters', {
currentTab: currentTab as 'Tracks' | 'Albums' | 'Artists',
@@ -127,7 +125,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
hasActiveFilters && (
<XStack
onPress={() => {
trigger('impactLight')
triggerHaptic('impactLight')
// Clear filters only for the current tab
if (currentTab === 'Tracks') {
useLibraryStore.getState().setTracksFilters({
@@ -13,14 +13,12 @@ import { useCurrentTrack } from '../../../stores/player/queue'
import { useSharedValue, useAnimatedReaction, withTiming } from 'react-native-reanimated'
import { runOnJS } from 'react-native-worklets'
import Slider from '@jellify-music/react-native-reanimated-slider'
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
export default function Scrubber(): React.JSX.Element {
const seekTo = useSeekTo()
const nowPlaying = useCurrentTrack()
const trigger = useHapticFeedback()
const { position } = useProgress(UPDATE_INTERVAL)
const { duration } = nowPlaying!
@@ -38,7 +36,7 @@ export default function Scrubber(): React.JSX.Element {
}, [position])
useEffect(() => {
if (isSeeking.current) trigger('clockTick')
if (isSeeking.current) triggerHaptic('clockTick')
}, [displayPosition.value])
// Handle track changes
@@ -16,7 +16,7 @@ import { useSharedValue, withDelay, withSpring } from 'react-native-reanimated'
import type { SharedValue } from 'react-native-reanimated'
import { runOnJS } from 'react-native-worklets'
import { usePrevious, useSkip } from '../../../hooks/player/callbacks'
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
import { useCurrentTrack } from '../../../stores/player/queue'
import { useApi } from '../../../stores'
import { formatArtistNames } from '../../../utils/formatting/artist-names'
@@ -30,8 +30,6 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem
const api = useApi()
const skip = useSkip()
const previous = usePrevious()
const trigger = useHapticFeedback()
// local fallback if no shared value was provided
const localX = useSharedValue(0)
const x = swipeX ?? localX
@@ -53,11 +51,11 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem
) {
if (e.translationX > 0) {
x.value = withSpring(220)
runOnJS(trigger)('notificationSuccess')
runOnJS(triggerHaptic)('notificationSuccess')
runOnJS(skip)(undefined)
} else {
x.value = withSpring(-220)
runOnJS(trigger)('notificationSuccess')
runOnJS(triggerHaptic)('notificationSuccess')
runOnJS(previous)()
}
x.value = withDelay(160, withSpring(0))
+3 -4
View File
@@ -19,7 +19,7 @@ import Animated, {
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { runOnJS } from 'react-native-worklets'
import { usePrevious, useSkip } from '../../hooks/player/callbacks'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import Icon from '../Global/components/icon'
import { useCurrentTrack } from '../../stores/player/queue'
@@ -28,7 +28,6 @@ export default function PlayerScreen(): React.JSX.Element {
const skip = useSkip()
const previous = usePrevious()
const trigger = useHapticFeedback()
const nowPlaying = useCurrentTrack()
const isAndroid = Platform.OS === 'android'
@@ -73,12 +72,12 @@ export default function PlayerScreen(): React.JSX.Element {
if (e.translationX > 0) {
// Inverted: swipe right = previous
translateX.value = withSpring(220)
runOnJS(trigger)('notificationSuccess')
runOnJS(triggerHaptic)('notificationSuccess')
runOnJS(previous)()
} else {
// Inverted: swipe left = next
translateX.value = withSpring(-220)
runOnJS(trigger)('notificationSuccess')
runOnJS(triggerHaptic)('notificationSuccess')
runOnJS(skip)(undefined)
}
translateX.value = withDelay(160, withSpring(0))
+3 -5
View File
@@ -18,7 +18,7 @@ import useStreamingDeviceProfile from '../../stores/device-profile'
import { useEffect, useLayoutEffect, useState } from 'react'
import { updatePlaylist } from '../../../src/api/mutations/playlists'
import { usePlaylistTracks } from '../../../src/api/queries/playlist'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { InfiniteData, useMutation } from '@tanstack/react-query'
import Animated, {
Easing,
@@ -55,8 +55,6 @@ export default function Playlist({
const [playlistTracks, setPlaylistTracks] = useState<BaseItemDto[] | undefined>(undefined)
const trigger = useHapticFeedback()
const {
data: tracks,
isPending,
@@ -85,7 +83,7 @@ export default function Playlist({
)
},
onSuccess: (_, { playlist, tracks }) => {
trigger('notificationSuccess')
triggerHaptic('notificationSuccess')
// Refresh playlist component data
queryClient.setQueryData<InfiniteData<BaseItemDto[]>>(
@@ -103,7 +101,7 @@ export default function Playlist({
)
},
onError: () => {
trigger('notificationError')
triggerHaptic('notificationError')
setNewName(playlist.Name ?? '')
setPlaylistTracks(tracks ?? [])
},
+12 -31
View File
@@ -9,7 +9,7 @@ import JellifyTrack from '@/src/types/JellifyTrack'
import calculateTrackVolume from './functions/normalization'
import usePlayerEngineStore, { PlayerEngine } from '../../stores/player/engine'
import { useRemoteMediaClient } from 'react-native-google-cast'
import useHapticFeedback from '../use-haptic-feedback'
import { triggerHaptic } from '../use-haptic-feedback'
import { usePlayerQueueStore } from '../../stores/player/queue'
/**
@@ -20,10 +20,8 @@ export const useTogglePlayback = () => {
usePlayerEngineStore((state) => state.playerEngineData) === PlayerEngine.GOOGLE_CAST
const remoteClient = useRemoteMediaClient()
const trigger = useHapticFeedback()
return async (state: State | undefined) => {
trigger('impactMedium')
triggerHaptic('impactMedium')
if (state === State.Playing) {
if (isCasting && remoteClient) return await remoteClient.pause()
@@ -52,10 +50,8 @@ export const useTogglePlayback = () => {
}
export const useToggleRepeatMode = () => {
const trigger = useHapticFeedback()
return async () => {
trigger('impactLight')
triggerHaptic('impactLight')
const currentMode = await TrackPlayer.getRepeatMode()
let nextMode: RepeatMode
@@ -83,10 +79,8 @@ export const useSeekTo = () => {
usePlayerEngineStore((state) => state.playerEngineData) === PlayerEngine.GOOGLE_CAST
const remoteClient = useRemoteMediaClient()
const trigger = useHapticFeedback()
return async (position: number) => {
trigger('impactLight')
triggerHaptic('impactLight')
if (isCasting && remoteClient)
return await remoteClient.seek({
@@ -101,24 +95,20 @@ export const useSeekTo = () => {
* A mutation to handle seeking to a specific position in the track
*/
const useSeekBy = () => {
const trigger = useHapticFeedback()
return async (seekSeconds: number) => {
trigger('clockTick')
triggerHaptic('clockTick')
await TrackPlayer.seekBy(seekSeconds)
}
}
export const useAddToQueue = () => {
const trigger = useHapticFeedback()
return async (variables: AddToQueueMutation) => {
try {
if (variables.queuingType === QueuingType.PlayingNext) playNextInQueue({ ...variables })
else playLaterInQueue({ ...variables })
trigger('notificationSuccess')
triggerHaptic('notificationSuccess')
Toast.show({
text1:
variables.queuingType === QueuingType.PlayingNext
@@ -127,7 +117,7 @@ export const useAddToQueue = () => {
type: 'success',
})
} catch (error) {
trigger('notificationError')
triggerHaptic('notificationError')
console.error(
`Failed to ${variables.queuingType === QueuingType.PlayingNext ? 'play next' : 'add to queue'}`,
error,
@@ -148,38 +138,31 @@ export const useAddToQueue = () => {
}
export const useLoadNewQueue = () => {
const trigger = useHapticFeedback()
return async (variables: QueueMutation) => {
trigger('impactLight')
triggerHaptic('impactLight')
const { finalStartIndex, tracks } = await loadQueue({ ...variables })
}
}
export const usePrevious = () => {
const trigger = useHapticFeedback()
return async () => {
trigger('impactMedium')
triggerHaptic('impactMedium')
await previous()
}
}
export const useSkip = () => {
const trigger = useHapticFeedback()
return async (index?: number | undefined) => {
trigger('impactMedium')
triggerHaptic('impactMedium')
await skip(index)
}
}
export const useRemoveFromQueue = () => {
const trigger = useHapticFeedback()
return async (index: number) => {
trigger('impactMedium')
triggerHaptic('impactMedium')
await TrackPlayer.remove([index])
const prevQueue = usePlayerQueueStore.getState().queue
@@ -238,10 +221,8 @@ export const useResetQueue = () => async () => {
}
export const useToggleShuffle = () => {
const trigger = useHapticFeedback()
return async (shuffled: boolean) => {
trigger('impactMedium')
triggerHaptic('impactMedium')
if (shuffled) await handleDeshuffle()
else await handleShuffle()
-7
View File
@@ -11,10 +11,3 @@ export function triggerHaptic(type?: keyof typeof HapticFeedbackTypes | HapticFe
trigger(type)
}
}
/**
* @deprecated Use triggerHaptic() directly instead - it's not a hook anymore
*/
const useHapticFeedback = () => triggerHaptic
export default useHapticFeedback
-11
View File
@@ -1,11 +0,0 @@
import { useEffect, useRef } from 'react'
export function usePreviousValue(value: boolean) {
const previousValue = useRef(value)
useEffect(() => {
previousValue.current = value
}, [value])
return previousValue.current
}
+3 -5
View File
@@ -9,7 +9,7 @@ import { createPlaylist } from '../../api/mutations/playlists'
import Toast from 'react-native-toast-message'
import Icon from '../../components/Global/components/icon'
import LibraryStackParamList from './types'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { useUserPlaylists } from '../../api/queries/playlist'
import { useApi, useJellifyUser } from '../../stores'
import { isEmpty } from 'lodash'
@@ -25,12 +25,10 @@ export default function AddPlaylist({
const { refetch } = useUserPlaylists()
const trigger = useHapticFeedback()
const useAddPlaylist = useMutation({
mutationFn: ({ name }: { name: string }) => createPlaylist(api, user, name),
onSuccess: (data: void, { name }: { name: string }) => {
trigger('notificationSuccess')
triggerHaptic('notificationSuccess')
Toast.show({
text1: 'Playlist created',
@@ -44,7 +42,7 @@ export default function AddPlaylist({
refetch()
},
onError: () => {
trigger('notificationError')
triggerHaptic('notificationError')
},
})
+3 -5
View File
@@ -7,7 +7,7 @@ import { deletePlaylist } from '../../api/mutations/playlists'
import { queryClient } from '../../constants/query-client'
import Icon from '../../components/Global/components/icon'
import { DeletePlaylistProps } from '../types'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { useApi, useJellifyLibrary } from '../../stores'
import { UserPlaylistsQueryKey } from '../../api/queries/playlist/keys'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -20,12 +20,10 @@ export default function DeletePlaylist({
const [library] = useJellifyLibrary()
const trigger = useHapticFeedback()
const useDeletePlaylist = useMutation({
mutationFn: (playlist: BaseItemDto) => deletePlaylist(api, playlist.Id!),
onSuccess: (data: void, playlist: BaseItemDto) => {
trigger('notificationSuccess')
triggerHaptic('notificationSuccess')
navigation.goBack() // Dismiss modal
@@ -37,7 +35,7 @@ export default function DeletePlaylist({
})
},
onError: () => {
trigger('notificationError')
triggerHaptic('notificationError')
},
})