mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-06 02:50:30 -06:00
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:
@@ -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'
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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' } },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -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!],
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 () => {},
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user