Bugfix/moving track in queue doesnt work (#458)

Fixing an issue where moving tracks in the queue didn't work or would truncate the queue
This commit is contained in:
Violet Caulfield
2025-07-24 18:43:38 -05:00
committed by GitHub
parent 7236f129ae
commit 0e6f1d2bae
16 changed files with 233 additions and 373 deletions

View File

@@ -38,9 +38,9 @@ const QueueConsumer = () => {
<>
<Text testID='current-index'>{currentIndex}</Text>
<Button title='skip' testID='use-skip' onPress={() => useSkip.mutate(undefined)} />
<Button title='skip' testID='use-skip' onPress={() => useSkip()} />
<Button title='previous' testID='use-previous' onPress={() => usePrevious.mutate()} />
<Button title='previous' testID='use-previous' onPress={() => usePrevious()} />
<Button
title='load new queue'

View File

@@ -123,11 +123,7 @@ const TestComponent = () => {
onPress={() => setCurrentIndex(2)}
/>
<Button
title='Toggle Shuffle'
testID='toggle-shuffle'
onPress={() => useToggleShuffle.mutate()}
/>
<Button title='Toggle Shuffle' testID='toggle-shuffle' onPress={useToggleShuffle} />
</>
)
}

View File

@@ -1,42 +0,0 @@
import move from '../../src/providers/Player/utils/move'
const playQueue = [
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '1' } },
{ id: '2', index: 1, url: 'https://example.com', item: { Id: '2' } },
{ id: '3', index: 2, url: 'https://example.com', item: { Id: '3' } },
]
/**
* Tests the move track utility function
*
* Doesn't inspect the RNTP queue, only the play queue
*
* Doesn't inspect the track indexes, but rather the track IDs to ensure the correct track is moved
*/
describe('Move Track Util', () => {
afterEach(() => {
jest.clearAllMocks()
})
describe('moveTrack', () => {
it('should move the first track to the second index', () => {
const result = move(playQueue, 0, 1)
expect(result).toEqual([
{ id: '2', index: 1, url: 'https://example.com', item: { Id: '2' } },
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '1' } },
{ id: '3', index: 2, url: 'https://example.com', item: { Id: '3' } },
])
})
it('should move the last track to the first index', () => {
const result = move(playQueue, 2, 0)
expect(result).toEqual([
{ id: '3', index: 2, url: 'https://example.com', item: { Id: '3' } },
{ id: '1', index: 0, url: 'https://example.com', item: { Id: '1' } },
{ id: '2', index: 1, url: 'https://example.com', item: { Id: '2' } },
])
})
})
})

View File

@@ -61,4 +61,15 @@ appId: com.jellify
id: 'queue-item-12'
- pressKey: BACK
- assertVisible:
id: "previous-button-test-id"
- tapOn:
id: "previous-button-test-id"
- assertVisible:
id: "skip-button-test-id"
- tapOn:
id: "skip-button-test-id"
- pressKey: BACK

View File

@@ -21,7 +21,6 @@ import { mapDtoToTrack } from '../../utils/mappings'
import { useNetworkContext } from '../../providers/Network'
import { useSettingsContext } from '../../providers/Settings'
import { useQueueContext } from '../../providers/Player/queue'
import { usePlayerContext } from '../../providers/Player'
import { QueuingType } from '../../enums/queuing-type'
/**
@@ -47,7 +46,6 @@ export function AlbumScreen({ route, navigation }: HomeAlbumProps): React.JSX.El
} = useNetworkContext()
const { downloadQuality, streamingQuality } = useSettingsContext()
const { useLoadNewQueue } = useQueueContext()
const { useStartPlayback } = usePlayerContext()
const { data: discs, isPending } = useQuery({
queryKey: [QueryKeys.ItemTracks, album.Id!],

View File

@@ -14,10 +14,7 @@ import FastImage from 'react-native-fast-image'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
import { useNetworkContext } from '../../../providers/Network'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { useQueueContext } from '../../../providers/Player/queue'
import { fetchItem } from '../../../api/queries/item'
import { useJellifyContext } from '../../../providers'
import DownloadedIcon from './downloaded-icon'
@@ -54,8 +51,8 @@ export default function Track({
onRemove,
}: TrackProps): React.JSX.Element {
const theme = useTheme()
const { api, user } = useJellifyContext()
const { nowPlaying, useStartPlayback } = usePlayerContext()
const { api } = useJellifyContext()
const { nowPlaying } = usePlayerContext()
const { playQueue, useLoadNewQueue } = useQueueContext()
const { downloadedTracks, networkStatus } = useNetworkContext()

View File

@@ -3,9 +3,10 @@ import { usePlayerContext } from '../../../providers/Player'
import { getToken, useTheme, View, YStack, ZStack } from 'tamagui'
import { useColorScheme } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import { getPrimaryBlurhashFromDto } from '../../../utils/blurhash'
import { BlurView } from 'blur-react-native'
import ItemImage from '../../Global/components/image'
import { useSettingsContext } from '../../../providers/Settings'
export default function BlurredBackground({
width,
height,
@@ -14,8 +15,10 @@ export default function BlurredBackground({
height: number
}): React.JSX.Element {
const { nowPlaying } = usePlayerContext()
const { theme: themeSetting } = useSettingsContext()
const theme = useTheme()
const isDarkMode = useColorScheme() === 'dark'
const isDarkMode =
themeSetting === 'dark' || (themeSetting === 'system' && useColorScheme() === 'dark')
return (
<ZStack flex={1} width={width} height={height}>
@@ -49,9 +52,11 @@ export default function BlurredBackground({
position='absolute'
top={0}
left={0}
bottom={0}
right={0}
bottom={0}
backgroundColor={theme.background.val}
width={width}
height={height}
opacity={0.5}
/>
)}

View File

@@ -24,7 +24,7 @@ export default function PlayPauseButton({
size={size}
name='pause'
testID='pause-button-test-id'
onPress={() => useTogglePlayback.mutate(undefined)}
onPress={useTogglePlayback}
/>
)
break
@@ -48,7 +48,7 @@ export default function PlayPauseButton({
size={size}
name='play'
testID='play-button-test-id'
onPress={() => useTogglePlayback.mutate(undefined)}
onPress={useTogglePlayback}
/>
)
break

View File

@@ -3,27 +3,14 @@ import { Spacer, XStack, getToken } from 'tamagui'
import PlayPauseButton from './buttons'
import Icon from '../../Global/components/icon'
import { usePlayerContext } from '../../../providers/Player'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { useQueueContext } from '../../../providers/Player/queue'
import { RepeatMode } from 'react-native-track-player'
export default function Controls(): React.JSX.Element {
const { width } = useSafeAreaFrame()
const { useSeekBy } = usePlayerContext()
const { usePrevious, useSkip } = useQueueContext()
const { nowPlaying, useToggleShuffle, useToggleRepeatMode, repeatMode } = usePlayerContext()
const { useToggleShuffle, useToggleRepeatMode, repeatMode } = usePlayerContext()
const {
playQueue,
setPlayQueue,
currentIndex,
setCurrentIndex,
shuffled,
setShuffled,
unshuffledQueue,
} = useQueueContext()
const { shuffled } = useQueueContext()
return (
<XStack
@@ -37,7 +24,7 @@ export default function Controls(): React.JSX.Element {
small
color={shuffled ? '$primary' : '$color'}
name='shuffle'
onPress={() => useToggleShuffle.mutate()}
onPress={useToggleShuffle}
/>
<Spacer />
@@ -45,8 +32,9 @@ export default function Controls(): React.JSX.Element {
<Icon
name='skip-previous'
color='$primary'
onPress={() => usePrevious.mutate()}
onPress={() => usePrevious()}
large
testID='previous-button-test-id'
/>
{/* I really wanted a big clunky play button */}
@@ -55,8 +43,9 @@ export default function Controls(): React.JSX.Element {
<Icon
name='skip-next'
color='$primary'
onPress={() => useSkip.mutate(undefined)}
onPress={() => useSkip()}
large
testID='skip-button-test-id'
/>
<Spacer />
@@ -65,7 +54,7 @@ export default function Controls(): React.JSX.Element {
small
color={repeatMode === RepeatMode.Off ? '$color' : '$primary'}
name={repeatMode === RepeatMode.Track ? 'repeat-once' : 'repeat'}
onPress={() => useToggleRepeatMode.mutate()}
onPress={useToggleRepeatMode}
/>
</XStack>
)

View File

@@ -9,8 +9,6 @@ import { usePlayerContext } from '../../../providers/Player'
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
import { UPDATE_INTERVAL } from '../../../player/config'
import { ProgressMultiplier } from '../component.config'
import { useQueueContext } from '../../../providers/Player/queue'
import { Platform } from 'react-native'
import { useSettingsContext } from '../../../providers/Settings'
// Create a simple pan gesture
@@ -18,7 +16,6 @@ const scrubGesture = Gesture.Pan().runOnJS(true)
export default function Scrubber(): React.JSX.Element {
const { useSeekTo, nowPlaying } = usePlayerContext()
const { useSkip, usePrevious } = useQueueContext()
const { width } = useSafeAreaFrame()
const { reducedHaptics } = useSettingsContext()
@@ -49,13 +46,11 @@ export default function Scrubber(): React.JSX.Element {
if (
!isUserInteractingRef.current &&
Date.now() - lastSeekTimeRef.current > 200 && // 200ms debounce after seeking
!useSeekTo.isPending &&
!useSkip.isPending &&
!usePrevious.isPending
!useSeekTo.isPending
) {
setDisplayPosition(calculatedPosition)
}
}, [calculatedPosition, useSeekTo.isPending, useSkip.isPending, usePrevious.isPending])
}, [calculatedPosition, useSeekTo.isPending])
// Handle track changes
useEffect(() => {
@@ -100,7 +95,7 @@ export default function Scrubber(): React.JSX.Element {
<YStack>
<HorizontalSlider
value={displayPosition}
max={maxDuration}
max={maxDuration ? maxDuration : 1 * ProgressMultiplier}
width={getToken('$20') + getToken('$20')}
props={{
maxWidth: width / 1.1,

View File

@@ -1,31 +1,14 @@
import { StackParamList } from '../types'
import { usePlayerContext } from '../../providers/Player'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React, { useCallback, useMemo, useState } from 'react'
import { SafeAreaView, useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context'
import {
YStack,
XStack,
Spacer,
getTokens,
getToken,
useTheme,
ZStack,
useWindowDimensions,
View,
} from 'tamagui'
import { Text } from '../Global/helpers/text'
import Icon from '../Global/components/icon'
import FavoriteButton from '../Global/components/favorite-button'
import TextTicker from 'react-native-text-ticker'
import { TextTickerConfig } from './component.config'
import React, { useCallback, useState } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { YStack, XStack, getToken, useTheme, ZStack, useWindowDimensions, View } from 'tamagui'
import Scrubber from './components/scrubber'
import Controls from './components/controls'
import { useQueueContext } from '../../providers/Player/queue'
import Toast from 'react-native-toast-message'
import JellifyToastConfig from '../../constants/toast.config'
import { useFocusEffect } from '@react-navigation/native'
import { useJellifyContext } from '../../providers'
import Footer from './components/footer'
import BlurredBackground from './components/blurred-background'
import PlayerHeader from './components/header'
@@ -36,14 +19,10 @@ export default function PlayerScreen({
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { api } = useJellifyContext()
const [showToast, setShowToast] = useState(true)
const { nowPlaying } = usePlayerContext()
const { queueRef } = useQueueContext()
const theme = useTheme()
useFocusEffect(

View File

@@ -38,7 +38,7 @@ export const Miniplayer = React.memo(function Miniplayer({
const { nowPlaying } = usePlayerContext()
const { useSkip, usePrevious } = useQueueContext()
// Get progress from the track player with the specified update interval
const progress = useProgress(UPDATE_INTERVAL, false)
const progress = useProgress(UPDATE_INTERVAL)
const { width } = useWindowDimensions()
const translateX = useSharedValue(0)
@@ -48,10 +48,10 @@ export const Miniplayer = React.memo(function Miniplayer({
(direction: string) => {
if (direction === 'Swiped Left') {
// Skip to previous song
usePrevious.mutate()
usePrevious()
} else if (direction === 'Swiped Right') {
// Skip to next song
useSkip.mutate(undefined)
useSkip()
} else if (direction === 'Swiped Up') {
// Navigate to the big player
navigation.navigate('Player')

View File

@@ -6,13 +6,9 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import DraggableFlatList from 'react-native-draggable-flatlist'
import { Separator, XStack } from 'tamagui'
import { useQueueContext } from '../../providers/Player/queue'
import Animated from 'react-native-reanimated'
import { Gesture } from 'react-native-gesture-handler'
import { useState } from 'react'
import { trigger } from 'react-native-haptic-feedback'
import { isUndefined } from 'lodash'
const gesture = Gesture.Pan().runOnJS(true)
import { useLayoutEffect } from 'react'
export default function Queue({
navigation,
@@ -30,89 +26,81 @@ export default function Queue({
useSkip,
} = useQueueContext()
navigation.setOptions({
headerRight: () => {
return (
<Icon
name='notification-clear-all'
onPress={() => {
useRemoveUpcomingTracks.mutate()
}}
/>
)
},
})
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => {
return (
<Icon
name='notification-clear-all'
onPress={() => {
useRemoveUpcomingTracks.mutate()
}}
/>
)
},
})
}, [navigation])
const scrollIndex = playQueue.findIndex(
(queueItem) => queueItem.item.Id! === nowPlaying!.item.Id!,
)
const [isReordering, setIsReordering] = useState(false)
return (
<Animated.View>
<DraggableFlatList
contentInsetAdjustmentBehavior='automatic'
data={playQueue}
dragHitSlop={{
left: -50, // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
}}
extraData={nowPlaying}
// enableLayoutAnimationExperimental
getItemLayout={(data, index) => ({
length: 20,
offset: (20 / 9) * index,
index,
})}
initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0}
ItemSeparatorComponent={() => <Separator />}
// itemEnteringAnimation={FadeIn}
// itemExitingAnimation={FadeOut}
// itemLayoutAnimation={SequencedTransition}
keyExtractor={({ item }, index) => {
return `${index}-${item.Id}`
}}
numColumns={1}
onDragBegin={() => {
// setIsReordering(true)
}}
onDragEnd={({ from, to }) => {
setIsReordering(false)
useReorderQueue.mutate({ from, to })
}}
renderItem={({ item: queueItem, getIndex, drag, isActive }) => (
<XStack
alignItems='center'
onLongPress={(event) => {
<DraggableFlatList
contentInsetAdjustmentBehavior='automatic'
data={playQueue}
dragHitSlop={{
left: -50, // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
}}
extraData={nowPlaying}
// enableLayoutAnimationExperimental
getItemLayout={(data, index) => ({
length: 20,
offset: (20 / 9) * index,
index,
})}
initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0}
ItemSeparatorComponent={() => <Separator />}
// itemEnteringAnimation={FadeIn}
// itemExitingAnimation={FadeOut}
// itemLayoutAnimation={SequencedTransition}
keyExtractor={({ item }, index) => `${index}-${item.Id}`}
numColumns={1}
onDragEnd={({ from, to }) => {
useReorderQueue({ from, to })
}}
renderItem={({ item: queueItem, getIndex, drag, isActive }) => (
<XStack
alignItems='center'
onLongPress={(event) => {
trigger('impactLight')
drag()
}}
>
<Track
queue={queueRef}
navigation={navigation}
track={queueItem.item}
index={getIndex() ?? 0}
showArtwork
testID={`queue-item-${getIndex()}`}
onPress={() => {
const index = getIndex()
if (!isUndefined(index)) useSkip(index)
}}
onLongPress={() => {
trigger('impactLight')
drag()
}}
>
<Track
queue={queueRef}
navigation={navigation}
track={queueItem.item}
index={getIndex() ?? 0}
showArtwork
testID={`queue-item-${getIndex()}`}
onPress={() => {
const index = getIndex()
if (!isUndefined(index)) useSkip.mutate(index)
}}
onLongPress={() => {
trigger('impactLight')
drag()
}}
isNested
showRemove
onRemove={() => {
const index = getIndex()
if (!isUndefined(index)) useRemoveFromQueue.mutate(index)
}}
/>
</XStack>
)}
/>
</Animated.View>
isNested
showRemove
onRemove={() => {
const index = getIndex()
if (!isUndefined(index)) useRemoveFromQueue.mutate(index)
}}
/>
</XStack>
)}
/>
)
}

View File

@@ -35,6 +35,7 @@ import {
getTracksToPreload,
shouldStartPrefetching,
optimizePlayerQueue,
ensureUpcomingTracksInQueue,
} from '../../player/helpers/gapless'
import {
PREFETCH_THRESHOLD_SECONDS,
@@ -48,10 +49,9 @@ interface PlayerContext {
playbackState: State | undefined
repeatMode: RepeatMode
shuffled: boolean
useToggleRepeatMode: UseMutationResult<void, Error, void, unknown>
useToggleShuffle: UseMutationResult<void, Error, void, unknown>
useStartPlayback: () => void
useTogglePlayback: UseMutationResult<void, Error, void, unknown>
useToggleRepeatMode: () => void
useToggleShuffle: () => void
useTogglePlayback: () => void
useSeekTo: UseMutationResult<void, Error, number, unknown>
useSeekBy: UseMutationResult<void, Error, number, unknown>
}
@@ -208,7 +208,6 @@ const PlayerContextInitializer = () => {
// Prepare the next few tracks in TrackPlayer for smooth transitions
try {
const { ensureUpcomingTracksInQueue } = await import('../../player/helpers/gapless')
await ensureUpcomingTracksInQueue(newShuffledQueue, currentIndex)
} catch (error) {
console.warn('Failed to prepare upcoming tracks after shuffle:', error)
@@ -271,7 +270,6 @@ const PlayerContextInitializer = () => {
// Optionally, prepare the next few tracks in TrackPlayer for smooth transitions
try {
const { ensureUpcomingTracksInQueue } = await import('../../player/helpers/gapless')
await ensureUpcomingTracksInQueue(unshuffledQueue, newCurrentIndex)
} catch (error) {
console.warn('Failed to prepare upcoming tracks after deshuffle:', error)
@@ -365,7 +363,7 @@ const PlayerContextInitializer = () => {
/**
* A mutation to handle toggling the playback state
*/
const useTogglePlayback = useMutation({
const { mutate: useTogglePlayback } = useMutation({
mutationFn: async () => {
trigger('impactMedium')
@@ -385,13 +383,13 @@ const PlayerContextInitializer = () => {
},
})
const useToggleRepeatMode = useMutation({
const { mutate: useToggleRepeatMode } = useMutation({
mutationFn: async () => {
await toggleRepeatMode()
},
})
const useToggleShuffle = useMutation({
const { mutate: useToggleShuffle } = useMutation({
mutationFn: async () => {
try {
if (shuffled) {
@@ -531,14 +529,14 @@ const PlayerContextInitializer = () => {
}
// Optimize the TrackPlayer queue for smooth transitions
if (timeRemaining <= QUEUE_PREPARATION_THRESHOLD_SECONDS) {
console.debug(
`Gapless: Optimizing player queue (${timeRemaining}s remaining)`,
)
optimizePlayerQueue(playQueue, currentIndex).catch((error) =>
console.warn('Failed to optimize player queue:', error),
)
}
// if (timeRemaining <= QUEUE_PREPARATION_THRESHOLD_SECONDS) {
// console.debug(
// `Gapless: Optimizing player queue (${timeRemaining}s remaining)`,
// )
// optimizePlayerQueue(playQueue, currentIndex).catch((error) =>
// console.warn('Failed to optimize player queue:', error),
// )
// }
}
break
@@ -642,62 +640,10 @@ export const PlayerContext = createContext<PlayerContext>({
nowPlaying: undefined,
repeatMode: RepeatMode.Off,
shuffled: false,
useToggleRepeatMode: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useToggleRepeatMode: () => {},
playbackState: undefined,
useStartPlayback: () => {},
useTogglePlayback: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useToggleShuffle: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useTogglePlayback: () => {},
useToggleShuffle: () => {},
useSeekTo: {
mutate: () => {},
mutateAsync: async () => {},

View File

@@ -17,7 +17,6 @@ import { findPlayQueueIndexStart } from './utils'
import { trigger } from 'react-native-haptic-feedback'
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
import { markItemPlayed } from '../../api/mutations/item'
import { filterTracksOnNetworkStatus } from './utils/queue'
import { shuffleJellifyTracks } from './utils/shuffle'
import { SKIP_TO_PREVIOUS_THRESHOLD } from '../../player/config'
@@ -25,7 +24,6 @@ import { isUndefined } from 'lodash'
import Toast from 'react-native-toast-message'
import { useJellifyContext } from '..'
import { networkStatusTypes } from '@/src/components/Network/internetConnectionWatcher'
import move from './utils/move'
import { ensureUpcomingTracksInQueue } from '../../player/helpers/gapless'
/**
@@ -91,17 +89,17 @@ interface QueueContext {
/**
* A hook that reorders the queue
*/
useReorderQueue: UseMutationResult<void, Error, QueueOrderMutation, unknown>
useReorderQueue: (mutation: QueueOrderMutation) => void
/**
* A hook that skips to the next track
*/
useSkip: UseMutationResult<void, Error, number | undefined, unknown>
useSkip: (index?: number) => void
/**
* A hook that skips to the previous track
*/
usePrevious: UseMutationResult<void, Error, void, unknown>
usePrevious: () => void
/**
* A hook that sets the play queue
@@ -167,7 +165,7 @@ const QueueContextInitailizer = () => {
//#endregion State
//#region Context
const { api, sessionId, user } = useJellifyContext()
const { api, sessionId } = useJellifyContext()
const { downloadedTracks, networkStatus } = useNetworkContext()
const { downloadQuality, streamingQuality } = useSettingsContext()
@@ -449,18 +447,10 @@ const QueueContextInitailizer = () => {
} else await TrackPlayer.seekTo(0)
}
const skip = async (index?: number | undefined) => {
trigger('impactMedium')
console.debug(
`Skip to next triggered. Index is ${`using ${
!isUndefined(index) ? index : currentIndex
} as index ${!isUndefined(index) ? 'since it was provided' : ''}`}`,
)
const skip = async (index: number | undefined = undefined) => {
if (!isUndefined(index)) {
const track = playQueue[index]
const queue = await TrackPlayer.getQueue()
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
const queueIndex = queue.findIndex((t) => t.item.Id === track.item.Id)
if (queueIndex !== -1) {
@@ -470,13 +460,10 @@ const QueueContextInitailizer = () => {
// Track not found - ensure upcoming tracks are properly ordered
console.debug('Track not found in TrackPlayer queue, updating upcoming tracks')
try {
const { ensureUpcomingTracksInQueue } = await import(
'../../player/helpers/gapless'
)
await ensureUpcomingTracksInQueue(playQueue, currentIndex)
// Now try to find the track again
const updatedQueue = await TrackPlayer.getQueue()
const updatedQueue = (await TrackPlayer.getQueue()) as JellifyTrack[]
const updatedQueueIndex = updatedQueue.findIndex(
(t) => t.item.Id === track.item.Id,
)
@@ -510,21 +497,25 @@ const QueueContextInitailizer = () => {
},
onSuccess: (data, { queuingType }) => {
trigger('notificationSuccess')
// Burnt.alert({
// title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
// duration: 0.5,
// preset: 'done',
// })
console.debug(
`${queuingType === QueuingType.PlayingNext ? 'Played next' : 'Added to queue'}`,
)
Toast.show({
text1: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
type: 'success',
})
},
onError: () => {
onError: async (error, { queuingType }) => {
trigger('notificationError')
console.error(
`Failed to ${queuingType === QueuingType.PlayingNext ? 'play next' : 'add to queue'}`,
error,
)
Toast.show({
text1: 'Failed to add to queue',
text1:
queuingType === QueuingType.PlayingNext
? 'Failed to play next'
: 'Failed to add to queue',
type: 'error',
})
},
@@ -539,12 +530,15 @@ const QueueContextInitailizer = () => {
queue,
shuffled,
}: QueueMutation) => loadQueue(tracklist, queue, index, shuffled),
onSuccess: async (data, { queue, startPlayback }: QueueMutation) => {
onSuccess: async (data, { startPlayback }: QueueMutation) => {
trigger('notificationSuccess')
console.debug(`Loaded new queue`)
startPlayback && (await TrackPlayer.play())
if (typeof queue === 'object' && api && user) await markItemPlayed(api, user, queue)
},
onError: async (error) => {
trigger('notificationError')
console.error('Failed to load new queue:', error)
},
})
@@ -569,6 +563,12 @@ const QueueContextInitailizer = () => {
// Then update RNTP
await TrackPlayer.remove([index])
},
onSuccess: async (data, index: number) => {
console.debug(`Removed track at index ${index}`)
},
onError: async (error, index: number) => {
console.error(`Failed to remove track at index ${index}:`, error)
},
})
/**
@@ -599,22 +599,58 @@ const QueueContextInitailizer = () => {
onSuccess: () => {
trigger('notificationSuccess')
},
onError: async (error) => {
trigger('notificationError')
console.error('Failed to remove upcoming tracks:', error)
await ensureUpcomingTracksInQueue(playQueue, currentIndex)
},
})
const useReorderQueue = useMutation({
const { mutate: useReorderQueue } = useMutation({
mutationFn: async ({ from, to }: QueueOrderMutation) => {
console.debug(`Moving track from ${from} to ${to}`)
console.debug(
`TrackPlayer.move(${from}, ${to}) - Queue before move:`,
(await TrackPlayer.getQueue()).length,
)
// Update app state first to prevent race conditions
const newQueue = move(playQueue, from, to)
setPlayQueue(newQueue)
// Then update RNTP
await TrackPlayer.move(from, to)
const newQueue = (await TrackPlayer.getQueue()) as JellifyTrack[]
console.debug(`TrackPlayer.move(${from}, ${to}) - Queue after move:`, newQueue.length)
return newQueue
},
onSuccess: () => {
onMutate: async ({ from, to }) => {
console.debug(`Reordering queue from ${from} to ${to}`)
console.debug(`App queue before reorder:`, playQueue.length)
setSkipping(true)
},
onSuccess: async (newQueue, { from, to }) => {
trigger('notificationSuccess')
console.debug(`Reordered queue from ${from} to ${to} successfully`)
console.debug(`App queue after reorder:`, newQueue.length)
const newCurrentIndex = newQueue.findIndex(
(track) => track.item.Id === playQueue[currentIndex].item.Id,
)
if (newCurrentIndex !== -1) setCurrentIndex(newCurrentIndex)
setPlayQueue(newQueue)
},
onError: async (error) => {
trigger('notificationError')
console.error('Failed to reorder queue:', error)
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
setPlayQueue(queue)
},
onSettled: () => {
setSkipping(false)
},
networkMode: 'always',
retry: false,
})
const { mutate: resetQueue } = useMutation({
@@ -628,12 +664,36 @@ const QueueContextInitailizer = () => {
},
})
const useSkip = useMutation({
const { mutate: useSkip } = useMutation({
mutationFn: skip,
onMutate: (index: number | undefined = undefined) => {
trigger('impactMedium')
console.debug(
`Skip to next triggered. Index is ${`using ${
!isUndefined(index) ? index : currentIndex
} as index ${!isUndefined(index) ? 'since it was provided' : ''}`}`,
)
},
onSuccess: async () => {
console.debug('Skipped to next track')
},
onError: async (error) => {
console.error('Failed to skip to next track:', error)
},
networkMode: 'always',
gcTime: 0,
retry: false,
})
const usePrevious = useMutation({
const { mutate: usePrevious } = useMutation({
mutationFn: previous,
onSuccess: async () => {
console.debug('Skipped to previous track')
},
onError: async (error) => {
console.error('Failed to skip to previous track:', error)
},
})
//#endregion Hooks
@@ -737,42 +797,8 @@ export const QueueContext = createContext<QueueContext>({
submittedAt: 0,
},
useLoadNewQueue: () => {},
useSkip: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
usePrevious: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useSkip: () => {},
usePrevious: () => {},
useRemoveFromQueue: {
mutate: () => {},
mutateAsync: async () => {},
@@ -809,24 +835,7 @@ export const QueueContext = createContext<QueueContext>({
failureReason: null,
submittedAt: 0,
},
useReorderQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useReorderQueue: () => {},
shuffled: false,
setShuffled: () => {},
unshuffledQueue: [],

View File

@@ -1,11 +0,0 @@
import JellifyTrack from '../../../types/JellifyTrack'
const move = (queue: JellifyTrack[], from: number, to: number): JellifyTrack[] => {
const queueCopy = [...queue]
const movedTrack = queueCopy.splice(from, 1)[0]
queueCopy.splice(to, 0, movedTrack)
return queueCopy
}
export default move