Make violet suck less (#708)

making the queue list items more easily dragged
This commit is contained in:
Ritesh Shukla
2025-11-23 20:10:07 +05:30
committed by GitHub
parent 586da19caa
commit ca46bc2054
6 changed files with 68 additions and 93 deletions

View File

@@ -4,7 +4,7 @@ import 'react-native-url-polyfill/auto'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import Jellify from './src/components/jellify'
import { TamaguiProvider } from 'tamagui'
import { Platform, useColorScheme } from 'react-native'
import { LogBox, Platform, useColorScheme } from 'react-native'
import jellifyConfig from './tamagui.config'
import { queryClientPersister } from './src/constants/storage'
import { ONE_DAY, queryClient } from './src/constants/query-client'
@@ -26,6 +26,8 @@ import navigationRef from './navigation'
import { PROGRESS_UPDATE_EVENT_INTERVAL } from './src/player/config'
import { useThemeSetting } from './src/stores/settings/app'
LogBox.ignoreAllLogs()
export default function App(): React.JSX.Element {
// Add performance monitoring to track app-level re-renders
const performanceMetrics = usePerformanceMonitor('App', 3)

View File

@@ -85,6 +85,7 @@
"react-native-reanimated": "4.1.5",
"react-native-safe-area-context": "5.6.2",
"react-native-screens": "4.18.0",
"react-native-sortables": "^1.9.3",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.15.0",
"react-native-toast-message": "^2.3.3",
@@ -144,4 +145,4 @@
"node": ">=18"
},
"packageManager": "yarn@1.22.22"
}
}

View File

@@ -37,7 +37,7 @@ export interface TrackProps {
index: number
queue: Queue
showArtwork?: boolean | undefined
onPress?: () => void | undefined
onPress?: () => Promise<void> | undefined
onLongPress?: () => void | undefined
isNested?: boolean | undefined
invertedColors?: boolean | undefined

View File

@@ -2,10 +2,8 @@ import Icon from '../Global/components/icon'
import Track from '../Global/components/track'
import { RootStackParamList } from '../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import DraggableFlatList, { RenderItemParams } from 'react-native-draggable-flatlist'
import { Separator, XStack } from 'tamagui'
import { isUndefined } from 'lodash'
import { useLayoutEffect, useCallback, useMemo } from 'react'
import { ScrollView, XStack } from 'tamagui'
import { useLayoutEffect, useCallback, useState } from 'react'
import JellifyTrack from '../../types/JellifyTrack'
import {
useRemoveFromQueue,
@@ -13,24 +11,31 @@ import {
useReorderQueue,
useSkip,
} from '../../providers/Player/hooks/mutations'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { useCurrentTrack, usePlayQueue, useQueueRef } from '../../stores/player/queue'
import {
useCurrentTrack,
usePlayerQueueStore,
usePlayQueue,
useQueueRef,
} from '../../stores/player/queue'
import Sortable from 'react-native-sortables'
import { RenderItemInfo } from 'react-native-sortables/dist/typescript/types'
import { useReducedHapticsSetting } from '../../stores/settings/app'
export default function Queue({
navigation,
}: {
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const nowPlaying = useCurrentTrack()
const playQueue = usePlayerQueueStore.getState().queue
const [queue, setQueue] = useState<JellifyTrack[]>(playQueue)
const playQueue = usePlayQueue()
const queueRef = useQueueRef()
const { mutate: removeUpcomingTracks } = useRemoveUpcomingTracks()
const { mutate: removeFromQueue } = useRemoveFromQueue()
const { mutate: reorderQueue } = useReorderQueue()
const skip = useSkip()
const trigger = useHapticFeedback()
const [reducedHaptics] = useReducedHapticsSetting()
useLayoutEffect(() => {
navigation.setOptions({
@@ -40,96 +45,55 @@ export default function Queue({
})
}, [navigation, removeUpcomingTracks])
// Memoize scroll index calculation
const scrollIndex = useMemo(
() => playQueue?.findIndex((queueItem) => queueItem.item.Id! === nowPlaying!.item.Id!),
[playQueue, nowPlaying?.item.Id],
)
// Memoize key extractor for better performance
const keyExtractor = useCallback(
(item: JellifyTrack, index: number) => `${index}-${item.item.Id}`,
[],
)
// Memoize getItemLayout for better performance
const getItemLayout = useCallback(
(data: ArrayLike<JellifyTrack> | null | undefined, index: number) => ({
length: 20,
offset: (20 / 9) * index,
index,
}),
[],
)
// Memoize ItemSeparatorComponent to prevent recreation
const ItemSeparatorComponent = useCallback(() => <Separator />, [])
// Memoize onDragEnd handler
const handleDragEnd = useCallback(
({ from, to }: { from: number; to: number }) => {
reorderQueue({ from, to })
const handleOrderChange = useCallback(
({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => {
reorderQueue({ from: fromIndex, to: toIndex })
},
[reorderQueue],
)
const keyExtractor = useCallback((item: JellifyTrack) => `${item.item.Id}`, [])
// Memoize renderItem function for better performance
const renderItem = useCallback(
({ item: queueItem, getIndex, drag, isActive }: RenderItemParams<JellifyTrack>) => {
const index = getIndex()
({ item: queueItem, index }: RenderItemInfo<JellifyTrack>) => (
<XStack alignItems='center' key={`${index}-${queueItem.item.Id}`}>
<Sortable.Handle style={{ display: 'flex', flexShrink: 1 }}>
<Icon name='drag' />
</Sortable.Handle>
const handleLongPress = () => {
trigger('impactLight')
drag()
}
const handlePress = () => {
if (!isUndefined(index)) skip(index)
}
const handleRemove = () => {
if (!isUndefined(index)) removeFromQueue(index)
}
return (
<XStack alignItems='center' onLongPress={handleLongPress}>
<Track
queue={queueRef ?? 'Recently Played'}
track={queueItem.item}
index={index ?? 0}
showArtwork
testID={`queue-item-${index}`}
onPress={handlePress}
onLongPress={handleLongPress}
isNested
showRemove
onRemove={handleRemove}
/>
</XStack>
)
},
<Track
queue={queueRef ?? 'Recently Played'}
track={queueItem.item}
index={index ?? 0}
showArtwork
testID={`queue-item-${index}`}
onPress={() => skip(index)}
isNested
showRemove
onRemove={() => removeFromQueue(index)}
/>
</XStack>
),
[queueRef, navigation, useSkip, useRemoveFromQueue],
)
return (
<DraggableFlatList
contentInsetAdjustmentBehavior='automatic'
data={playQueue ?? []}
dragHitSlop={{
left: -50, // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
}}
extraData={nowPlaying?.item.Id} // Only track the playing track ID, not the entire object
getItemLayout={getItemLayout}
initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0}
ItemSeparatorComponent={ItemSeparatorComponent}
keyExtractor={keyExtractor}
numColumns={1}
onDragEnd={handleDragEnd}
renderItem={renderItem}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
updateCellsBatchingPeriod={50}
/>
<ScrollView flex={1}>
<Sortable.Grid
data={queue}
columns={1}
keyExtractor={keyExtractor}
renderItem={renderItem}
onOrderChange={handleOrderChange}
onDragEnd={({ data }) => {
setQueue(data)
}}
overDrag='vertical'
customHandle
hapticsEnabled={!reducedHaptics}
/>
</ScrollView>
)
}

View File

@@ -4,6 +4,7 @@ import { mmkvStateStorage } from '../../constants/storage'
import { create } from 'zustand'
import { createJSONStorage, devtools, persist } from 'zustand/middleware'
import { RepeatMode } from 'react-native-track-player'
import { useShallow } from 'zustand/react/shallow'
type PlayerQueueStore = {
shuffled: boolean
@@ -76,7 +77,7 @@ export const usePlayerQueueStore = create<PlayerQueueStore>()(
),
)
export const usePlayQueue = () => usePlayerQueueStore((state) => state.queue)
export const usePlayQueue = () => usePlayerQueueStore(useShallow((state) => state.queue))
export const useShuffle = () => usePlayerQueueStore((state) => state.shuffled)

View File

@@ -8421,7 +8421,7 @@ react-native-google-cast@^4.9.1:
resolved "https://registry.yarnpkg.com/react-native-google-cast/-/react-native-google-cast-4.9.1.tgz#f1c453c11460a1f787accff80072492a4cd3e86c"
integrity sha512-/HvIKAaWHtG6aTNCxrNrqA2ftWGkfH0M/2iN+28pdGUXpKmueb33mgL1m8D4zzwEODQMcmpfoCsym1IwDvugBQ==
react-native-haptic-feedback@^2.3.3:
react-native-haptic-feedback@>=2.0.0, react-native-haptic-feedback@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.3.tgz#88b6876e91399a69bd1b551fe1681b2f3dc1214e"
integrity sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ==
@@ -8477,6 +8477,13 @@ react-native-screens@4.18.0:
react-freeze "^1.0.0"
warn-once "^0.1.0"
react-native-sortables@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/react-native-sortables/-/react-native-sortables-1.9.3.tgz#cb7ef05aa121ccd54f42664a162c40779376605f"
integrity sha512-VLhW9+3AVEaJNwwQSgN+n/Qe+YRB0C0mNWTjHhyzcZ+YjY4BmJao4bZxl5lD6EsfqZ1Ij6B2ZdxjNlSkUXrvow==
optionalDependencies:
react-native-haptic-feedback ">=2.0.0"
react-native-swipeable-item@^2.0.9:
version "2.0.9"
resolved "https://registry.yarnpkg.com/react-native-swipeable-item/-/react-native-swipeable-item-2.0.9.tgz#694a3f10333b47ba7f0e57d916fa39407b76a6d6"