mirror of
https://github.com/Jellify-Music/App.git
synced 2026-02-23 12:18:41 -06:00
Player Backend Improvements (#291)
Separating Queuing and Player logic, please report bugs if you experience playback issues or queue irregularities Fetching additional track metadata for use in later features, utilizing transcoding URLs reported by Jellyfin Disable NowPlaying in CarPlay on startup - this should be navigable yet in the CarPlay interface
This commit is contained in:
@@ -10,7 +10,7 @@ export async function fetchMediaInfo(itemId: string): Promise<PlaybackInfoRespon
|
||||
userId: Client.user?.id,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
console.debug(data)
|
||||
console.debug('Received media info response')
|
||||
resolve(data)
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
@@ -9,6 +8,9 @@ import BlurhashedImage from './blurhashed-image'
|
||||
import Icon from '../helpers/icon'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { State } from 'react-native-track-player'
|
||||
|
||||
export default function Item({
|
||||
item,
|
||||
@@ -19,7 +21,8 @@ export default function Item({
|
||||
queueName: string
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { usePlayNewQueue } = usePlayerContext()
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
@@ -48,12 +51,20 @@ export default function Item({
|
||||
}
|
||||
|
||||
case 'Audio': {
|
||||
usePlayNewQueue.mutate({
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
queue: 'Search',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
})
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
index: 0,
|
||||
queue: 'Search',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
useStartPlayback.mutate()
|
||||
},
|
||||
},
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ import { useNetworkContext } from '../../../components/Network/provider'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../../api/queries/functions/media'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
import { State } from 'react-native-track-player'
|
||||
|
||||
interface TrackProps {
|
||||
track: BaseItemDto
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
tracklist?: BaseItemDto[] | undefined
|
||||
index?: number | undefined
|
||||
index: number
|
||||
queue: Queue
|
||||
showArtwork?: boolean | undefined
|
||||
onPress?: () => void | undefined
|
||||
@@ -51,7 +53,8 @@ export default function Track({
|
||||
onRemove,
|
||||
}: TrackProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const { nowPlaying, playQueue, usePlayNewQueue } = usePlayerContext()
|
||||
const { nowPlaying, useStartPlayback } = usePlayerContext()
|
||||
const { playQueue, useLoadNewQueue } = useQueueContext()
|
||||
const { downloadedTracks, networkStatus } = useNetworkContext()
|
||||
|
||||
const isPlaying = nowPlaying?.item.Id === track.Id
|
||||
@@ -76,13 +79,20 @@ export default function Track({
|
||||
if (onPress) {
|
||||
onPress()
|
||||
} else {
|
||||
usePlayNewQueue.mutate({
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
})
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
useStartPlayback.mutate()
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}}
|
||||
onLongPress={
|
||||
|
||||
@@ -6,9 +6,10 @@ import HorizontalCardList from '../../../components/Global/components/horizontal
|
||||
import { ItemCard } from '../../../components/Global/components/item-card'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { H2 } from '../../../components/Global/helpers/text'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
|
||||
export default function FrequentlyPlayedTracks({
|
||||
navigation,
|
||||
@@ -17,7 +18,8 @@ export default function FrequentlyPlayedTracks({
|
||||
}): React.JSX.Element {
|
||||
const { frequentlyPlayed } = useHomeContext()
|
||||
|
||||
const { usePlayNewQueue } = usePlayerContext()
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
|
||||
return (
|
||||
<View>
|
||||
@@ -48,13 +50,20 @@ export default function FrequentlyPlayedTracks({
|
||||
subCaption={`${track.Artists?.join(', ')}`}
|
||||
squared
|
||||
onPress={() => {
|
||||
usePlayNewQueue.mutate({
|
||||
track,
|
||||
index,
|
||||
tracklist: frequentlyPlayed ?? [track],
|
||||
queue: 'On Repeat',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
})
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track,
|
||||
index,
|
||||
tracklist: frequentlyPlayed ?? [track],
|
||||
queue: 'On Repeat',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
useStartPlayback.mutate()
|
||||
},
|
||||
},
|
||||
)
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -9,15 +9,16 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
|
||||
export default function RecentlyPlayed({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { nowPlaying, usePlayNewQueue } = usePlayerContext()
|
||||
const { nowPlaying, useStartPlayback } = usePlayerContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const { recentTracks } = useHomeContext()
|
||||
|
||||
return useMemo(() => {
|
||||
@@ -49,13 +50,20 @@ export default function RecentlyPlayed({
|
||||
squared
|
||||
item={recentlyPlayedTrack}
|
||||
onPress={() => {
|
||||
usePlayNewQueue.mutate({
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks ?? [recentlyPlayedTrack],
|
||||
queue: 'Recently Played',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
})
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks ?? [recentlyPlayedTrack],
|
||||
queue: 'Recently Played',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
useStartPlayback.mutate()
|
||||
},
|
||||
},
|
||||
)
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -6,11 +6,9 @@ import { AlbumScreen } from '../Album'
|
||||
import { PlaylistScreen } from '../Playlist/screens'
|
||||
import { ProvidedHome } from './component'
|
||||
import DetailsScreen from '../ItemDetail/screen'
|
||||
import AddPlaylist from '../Library/components/add-playlist'
|
||||
import ArtistsScreen from '../Artists/screen'
|
||||
import TracksScreen from '../Tracks/screen'
|
||||
import { ArtistScreen } from '../Artist'
|
||||
import { OfflineList } from '../offlineList'
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParamList>()
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
@@ -33,6 +32,7 @@ import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
import { useNetworkContext } from '../../../components/Network/provider'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
|
||||
interface TrackOptionsProps {
|
||||
track: BaseItemDto
|
||||
@@ -67,7 +67,7 @@ export default function TrackOptions({
|
||||
queryFn: () => fetchUserPlaylists(),
|
||||
})
|
||||
|
||||
const { useAddToQueue } = usePlayerContext()
|
||||
const { useAddToQueue } = useQueueContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ import Icon from '../../../components/Global/helpers/icon'
|
||||
import { getProgress, seekBy, skipToNext } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
|
||||
export default function Controls(): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { usePrevious, useSeekTo } = usePlayerContext()
|
||||
const { useSeekTo } = usePlayerContext()
|
||||
|
||||
const { usePrevious } = useQueueContext()
|
||||
|
||||
return (
|
||||
<XStack alignItems='center' justifyContent='space-evenly' marginVertical={'$2'}>
|
||||
@@ -22,13 +25,7 @@ export default function Controls(): React.JSX.Element {
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
name='skip-previous'
|
||||
onPress={async () => {
|
||||
const progress = await getProgress()
|
||||
if (progress.position < 3) usePrevious.mutate()
|
||||
else {
|
||||
useSeekTo.mutate(0)
|
||||
}
|
||||
}}
|
||||
onPress={() => usePrevious.mutate()}
|
||||
large
|
||||
/>
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ import { usePlayerContext } from '../../../player/provider'
|
||||
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
|
||||
import { UPDATE_INTERVAL } from '../../../player/config'
|
||||
import { ProgressMultiplier } from '../component.config'
|
||||
import { useSharedValue } from 'react-native-reanimated'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
|
||||
const scrubGesture = Gesture.Pan()
|
||||
|
||||
export default function Scrubber(): React.JSX.Element {
|
||||
const { useSeekTo, useSkip, usePrevious } = usePlayerContext()
|
||||
const { useSeekTo } = usePlayerContext()
|
||||
|
||||
const { useSkip, usePrevious } = useQueueContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TextTickerConfig } from './component.config'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../api/client'
|
||||
import { useQueueContext } from '../../player/queue-provider'
|
||||
|
||||
export function Miniplayer({
|
||||
navigation,
|
||||
@@ -19,7 +20,8 @@ export function Miniplayer({
|
||||
}): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
const { nowPlaying, useSkip } = usePlayerContext()
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
const { useSkip } = useQueueContext()
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -15,13 +15,16 @@ import Controls from '../helpers/controls'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
|
||||
export default function PlayerScreen({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying, queue } = usePlayerContext()
|
||||
const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying } = usePlayerContext()
|
||||
|
||||
const { queueRef } = useQueueContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
@@ -57,9 +60,9 @@ export default function PlayerScreen({
|
||||
>
|
||||
{
|
||||
// If the Queue is a BaseItemDto, display the name of it
|
||||
typeof queue === 'object'
|
||||
? (queue as BaseItemDto).Name ?? 'Untitled'
|
||||
: queue
|
||||
typeof queueRef === 'object'
|
||||
? queueRef.Name ?? 'Untitled'
|
||||
: queueRef
|
||||
}
|
||||
</Text>
|
||||
</YStack>
|
||||
@@ -91,7 +94,7 @@ export default function PlayerScreen({
|
||||
</XStack>
|
||||
</>
|
||||
)
|
||||
}, [nowPlaying, queue])}
|
||||
}, [nowPlaying, queueRef])}
|
||||
|
||||
<XStack marginHorizontal={20} paddingVertical={5}>
|
||||
{/** Memoize TextTickers otherwise they won't animate due to the progress being updated in the PlayerContext */}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import DraggableFlatList from 'react-native-draggable-flatlist'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { Separator } from 'tamagui'
|
||||
import { useQueueContext } from '../../../player/queue-provider'
|
||||
import Animated from 'react-native-reanimated'
|
||||
|
||||
export default function Queue({
|
||||
navigation,
|
||||
@@ -14,15 +16,16 @@ export default function Queue({
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
|
||||
const {
|
||||
playQueue,
|
||||
queue,
|
||||
useClearQueue,
|
||||
queueRef,
|
||||
useRemoveUpcomingTracks,
|
||||
useRemoveFromQueue,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
nowPlaying,
|
||||
} = usePlayerContext()
|
||||
} = useQueueContext()
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
@@ -30,7 +33,7 @@ export default function Queue({
|
||||
<Icon
|
||||
name='notification-clear-all'
|
||||
onPress={() => {
|
||||
useClearQueue.mutate()
|
||||
useRemoveUpcomingTracks.mutate()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -42,50 +45,52 @@ export default function Queue({
|
||||
)
|
||||
|
||||
return (
|
||||
<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: width / 9,
|
||||
offset: (width / 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}
|
||||
onDragEnd={({ data, from, to }) => {
|
||||
useReorderQueue.mutate({ newOrder: data, from, to })
|
||||
}}
|
||||
renderItem={({ item: queueItem, getIndex, drag, isActive }) => (
|
||||
<Track
|
||||
queue={queue}
|
||||
navigation={navigation}
|
||||
track={queueItem.item}
|
||||
index={getIndex()}
|
||||
showArtwork
|
||||
onPress={() => {
|
||||
useSkip.mutate(getIndex())
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactLight')
|
||||
drag()
|
||||
}}
|
||||
isNested
|
||||
showRemove
|
||||
onRemove={() => {
|
||||
if (getIndex()) useRemoveFromQueue.mutate(getIndex()!)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<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: width / 9,
|
||||
offset: (width / 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}
|
||||
onDragEnd={({ data, from, to }) => {
|
||||
useReorderQueue.mutate({ newOrder: data, from, to })
|
||||
}}
|
||||
renderItem={({ item: queueItem, getIndex, drag, isActive }) => (
|
||||
<Track
|
||||
queue={queueRef}
|
||||
navigation={navigation}
|
||||
track={queueItem.item}
|
||||
index={getIndex() ?? 0}
|
||||
showArtwork
|
||||
onPress={() => {
|
||||
useSkip.mutate(getIndex() ?? 0)
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactLight')
|
||||
drag()
|
||||
}}
|
||||
isNested
|
||||
showRemove
|
||||
onRemove={() => {
|
||||
if (getIndex()) useRemoveFromQueue.mutate(getIndex()!)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Button from '../../Global/helpers/button'
|
||||
import Client from '../../../api/client'
|
||||
import { useJellifyContext } from '../../../components/provider'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { StackParamList } from '@/components/types'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
@@ -17,7 +17,6 @@ export default function SignOut(): React.JSX.Element {
|
||||
setLoggedIn(false)
|
||||
Client.signOut()
|
||||
TrackPlayer.reset()
|
||||
// navigation.navigate('Offline')
|
||||
}}
|
||||
>
|
||||
Sign Out
|
||||
|
||||
@@ -10,6 +10,7 @@ import { JellifyProvider, useJellifyContext } from './provider'
|
||||
import { ToastProvider } from '@tamagui/toast'
|
||||
import { JellifyUserDataProvider } from './user-data-provider'
|
||||
import { NetworkContextProvider } from './Network/provider'
|
||||
import { QueueProvider } from '../player/queue-provider'
|
||||
|
||||
export default function Jellify(): React.JSX.Element {
|
||||
return (
|
||||
@@ -30,9 +31,11 @@ function App(): React.JSX.Element {
|
||||
return loggedIn ? (
|
||||
<JellifyUserDataProvider>
|
||||
<NetworkContextProvider>
|
||||
<PlayerProvider>
|
||||
<Navigation />
|
||||
</PlayerProvider>
|
||||
<QueueProvider>
|
||||
<PlayerProvider>
|
||||
<Navigation />
|
||||
</PlayerProvider>
|
||||
</QueueProvider>
|
||||
</NetworkContextProvider>
|
||||
</JellifyUserDataProvider>
|
||||
) : (
|
||||
|
||||
@@ -3,8 +3,6 @@ import Player from './Player/stack'
|
||||
import { Tabs } from './tabs'
|
||||
import { StackParamList } from './types'
|
||||
import { useTheme } from 'tamagui'
|
||||
import DetailsScreen from './ItemDetail/screen'
|
||||
import { OfflineList } from './offlineList'
|
||||
|
||||
export default function Navigation(): React.JSX.Element {
|
||||
const RootStack = createNativeStackNavigator<StackParamList>()
|
||||
@@ -29,14 +27,6 @@ export default function Navigation(): React.JSX.Element {
|
||||
presentation: 'modal',
|
||||
}}
|
||||
/>
|
||||
<RootStack.Screen
|
||||
name='Offline'
|
||||
component={OfflineList}
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: 'modal',
|
||||
}}
|
||||
/>
|
||||
</RootStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { JellifyTrack } from '@/types/JellifyTrack'
|
||||
import React from 'react'
|
||||
import { FlatList, Pressable } from 'react-native'
|
||||
import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated'
|
||||
import { Image, Text, View, XStack, YStack } from 'tamagui'
|
||||
import { getAudioCache } from '../Network/offlineModeUtils'
|
||||
import { usePlayerContext } from '../../player/provider'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
interface Props {
|
||||
tracks: JellifyTrack[]
|
||||
onPress: (track: JellifyTrack) => void
|
||||
}
|
||||
|
||||
export function OfflineList() {
|
||||
const tracks = getAudioCache()
|
||||
const navigation = useNavigation()
|
||||
const { usePlayNewQueueOffline } = usePlayerContext()
|
||||
const onPress = (track: JellifyTrack) => {
|
||||
console.log('onPress', track)
|
||||
usePlayNewQueueOffline.mutate({ trackListOffline: track })
|
||||
navigation.navigate('Player')
|
||||
}
|
||||
|
||||
const renderItem = ({ item }: { item: JellifyTrack }) => (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(300)}
|
||||
exiting={FadeOut}
|
||||
layout={Layout.springify()}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
marginVertical: 6,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#1c1c1e',
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={() => onPress(item)}>
|
||||
<XStack padding={12} gap={12} alignItems='center'>
|
||||
<Image
|
||||
source={{ uri: item.artwork }}
|
||||
style={{ width: 64, height: 64, borderRadius: 12 }}
|
||||
/>
|
||||
<YStack flex={1}>
|
||||
<Text fontWeight='700' color='#fff' numberOfLines={1}>
|
||||
{item.title || 'Unknown Title'}
|
||||
</Text>
|
||||
<Text color='#bbb' numberOfLines={1}>
|
||||
{item.artist || 'Unknown Artist'}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
)
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={tracks}
|
||||
keyExtractor={(item) => item.item.Id as string}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: 100,
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -29,7 +29,6 @@ const JellifyContextInitializer = () => {
|
||||
|
||||
if (loggedIn) {
|
||||
CarPlay.setRootTemplate(CarPlayNavigation)
|
||||
CarPlay.pushTemplate(CarPlayNowPlaying)
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
CarPlay.enableNowPlaying(true) // https://github.com/birkir/react-native-carplay/issues/185
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
CF6B19F92D55121E002464CB /* icon_tinted_20pt_3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19EB2D55121E002464CB /* icon_tinted_20pt_3x.png */; };
|
||||
CF6B19FA2D55121E002464CB /* icon_tinted_60pt_3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19F12D55121E002464CB /* icon_tinted_60pt_3x.png */; };
|
||||
CF6B19FB2D55121E002464CB /* icon_tinted_40pt_2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19EE2D55121E002464CB /* icon_tinted_40pt_2x.png */; };
|
||||
CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */; };
|
||||
CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA452D3E99DF003D88B7 /* CarScene.swift */; };
|
||||
CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */; };
|
||||
CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */; };
|
||||
@@ -148,7 +147,6 @@
|
||||
CF6B19F12D55121E002464CB /* icon_tinted_60pt_3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_tinted_60pt_3x.png; sourceTree = "<group>"; };
|
||||
CF6B19F22D55121E002464CB /* icon_tinted_original.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_tinted_original.png; sourceTree = "<group>"; };
|
||||
CF7179092CBC486C0021BCA3 /* Jellify-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Jellify-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "dummy-rntp.swift"; sourceTree = "<group>"; };
|
||||
CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
CF98CA452D3E99DF003D88B7 /* CarScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarScene.swift; sourceTree = "<group>"; };
|
||||
CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneScene.swift; sourceTree = "<group>"; };
|
||||
@@ -204,7 +202,6 @@
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
||||
F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */,
|
||||
CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */,
|
||||
CF7179092CBC486C0021BCA3 /* Jellify-Bridging-Header.h */,
|
||||
);
|
||||
name = Jellify;
|
||||
@@ -522,10 +519,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
|
||||
@@ -539,10 +540,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
|
||||
@@ -607,7 +612,6 @@
|
||||
CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */,
|
||||
CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */,
|
||||
CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */,
|
||||
CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */,
|
||||
66BC9C5D1B536CD0799EEC89 /* ExpoModulesProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -843,10 +847,7 @@
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -932,10 +933,7 @@
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
//
|
||||
// dummy-rntp.swift
|
||||
// Jellify
|
||||
//
|
||||
// Created by Violet Caulfield on 10/13/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -49,7 +49,6 @@
|
||||
"invert-color": "^2.0.0",
|
||||
"jest-expo": "^52.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"npm-bundle": "^3.0.3",
|
||||
"patch-package": "^8.0.0",
|
||||
"react": "18.3.1",
|
||||
"react-freeze": "^1.0.4",
|
||||
|
||||
@@ -23,7 +23,7 @@ export function buildNewQueue(
|
||||
return newQueue
|
||||
}
|
||||
|
||||
export function networkStatusCheck(
|
||||
export function filterTracksOnNetworkStatus(
|
||||
networkStatus: networkStatusTypes | undefined,
|
||||
queuedItems: BaseItemDto[],
|
||||
downloadedTracks: JellifyDownload[],
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface QueueMutation {
|
||||
tracklist: BaseItemDto[]
|
||||
queue: Queue
|
||||
queuingType?: QueuingType | undefined
|
||||
trackListOffline?: JellifyTrack
|
||||
}
|
||||
|
||||
export interface AddToQueueMutation {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createContext, ReactNode, SetStateAction, useContext, useEffect, useSta
|
||||
import { JellifyTrack } from '../types/JellifyTrack'
|
||||
import { storage } from '../constants/storage'
|
||||
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'
|
||||
import { findPlayNextIndexStart, findPlayQueueIndexStart } from './helpers/index'
|
||||
import TrackPlayer, {
|
||||
Event,
|
||||
State,
|
||||
@@ -13,58 +12,27 @@ import { isEqual, isUndefined } from 'lodash'
|
||||
import { handlePlaybackProgressUpdated, handlePlaybackState } from './handlers'
|
||||
import { useUpdateOptions } from '../player/hooks'
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query'
|
||||
import { mapDtoToTrack } from '../helpers/mappings'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import {
|
||||
getActiveTrackIndex,
|
||||
getQueue,
|
||||
pause,
|
||||
seekTo,
|
||||
skip,
|
||||
skipToNext,
|
||||
skipToPrevious,
|
||||
} from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { pause, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { convertRunTimeTicksToSeconds } from '../helpers/runtimeticks'
|
||||
import Client from '../api/client'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
|
||||
import { Section } from '../components/Player/types'
|
||||
import { Queue } from './types/queue-item'
|
||||
|
||||
import * as Burnt from 'burnt'
|
||||
import { markItemPlayed } from '../api/mutations/functions/item'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { SKIP_TO_PREVIOUS_THRESHOLD } from './config'
|
||||
import { useNetworkContext } from '../components/Network/provider'
|
||||
import { networkStatusCheck } from './helpers/queue'
|
||||
import { QueueProvider } from './queue-provider'
|
||||
import { useQueueContext } from './queue-provider'
|
||||
|
||||
interface PlayerContext {
|
||||
initialized: boolean
|
||||
nowPlayingIsFavorite: boolean
|
||||
setNowPlayingIsFavorite: React.Dispatch<SetStateAction<boolean>>
|
||||
nowPlaying: JellifyTrack | undefined
|
||||
playQueue: JellifyTrack[]
|
||||
queue: Queue
|
||||
getQueueSectionData: () => Section[]
|
||||
useAddToQueue: UseMutationResult<void, Error, AddToQueueMutation, unknown>
|
||||
useClearQueue: UseMutationResult<void, Error, void, unknown>
|
||||
useRemoveFromQueue: UseMutationResult<void, Error, number, unknown>
|
||||
useReorderQueue: UseMutationResult<void, Error, QueueOrderMutation, unknown>
|
||||
useTogglePlayback: UseMutationResult<void, Error, number | undefined, unknown>
|
||||
useStartPlayback: UseMutationResult<void, Error, void, unknown>
|
||||
useTogglePlayback: UseMutationResult<void, Error, void, unknown>
|
||||
useSeekTo: UseMutationResult<void, Error, number, unknown>
|
||||
useSkip: UseMutationResult<void, Error, number | undefined, unknown>
|
||||
usePrevious: UseMutationResult<void, Error, void, unknown>
|
||||
usePlayNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>
|
||||
playbackState: State | undefined
|
||||
setNowPlaying: (track: JellifyTrack) => void
|
||||
}
|
||||
|
||||
const PlayerContextInitializer = () => {
|
||||
const nowPlayingJson = storage.getString(MMKVStorageKeys.NowPlaying)
|
||||
const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue)
|
||||
const queueJson = storage.getString(MMKVStorageKeys.Queue)
|
||||
|
||||
const playStateApi = getPlaystateApi(Client.api!)
|
||||
|
||||
@@ -76,146 +44,27 @@ const PlayerContextInitializer = () => {
|
||||
nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined,
|
||||
)
|
||||
|
||||
const [isSkipping, setIsSkipping] = useState<boolean>(false)
|
||||
const { playQueue, currentIndex } = useQueueContext()
|
||||
|
||||
const [playQueue, setPlayQueue] = useState<JellifyTrack[]>(
|
||||
playQueueJson ? JSON.parse(playQueueJson) : [],
|
||||
)
|
||||
|
||||
const [queue, setQueue] = useState<Queue>(queueJson ? JSON.parse(queueJson) : 'Queue')
|
||||
//#endregion State
|
||||
|
||||
//#region Functions
|
||||
const play = async (index?: number | undefined) => {
|
||||
if (index && index > 0) {
|
||||
TrackPlayer.skip(index)
|
||||
}
|
||||
|
||||
TrackPlayer.play()
|
||||
const play = async () => {
|
||||
await TrackPlayer.play()
|
||||
}
|
||||
|
||||
const getQueueSectionData: () => Section[] = () => {
|
||||
return Object.keys(QueuingType).map((type) => {
|
||||
return {
|
||||
title: type,
|
||||
data: playQueue.filter((track) => track.QueuingType === type),
|
||||
} as Section
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a {@link BaseItemDto} of a track on Jellyfin, and updates it's
|
||||
* position in the {@link queue}
|
||||
*
|
||||
*
|
||||
* @param track The Jellyfin track object to update and replace in the queue
|
||||
*/
|
||||
const replaceQueueItem: (track: BaseItemDto) => Promise<void> = async (track: BaseItemDto) => {
|
||||
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
|
||||
|
||||
const queueItemIndex = queue.findIndex((queuedTrack) => queuedTrack.item.Id === track.Id!)
|
||||
|
||||
// Update queued item at index if found, else silently do nothing
|
||||
if (queueItemIndex !== -1) {
|
||||
const queueItem = queue[queueItemIndex]
|
||||
|
||||
TrackPlayer.remove([queueItemIndex]).then(() => {
|
||||
TrackPlayer.add(
|
||||
mapDtoToTrack(track, downloadedTracks ?? [], queueItem.QueuingType),
|
||||
queueItemIndex,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetQueue = async (hideMiniplayer?: boolean | undefined) => {
|
||||
console.debug('Clearing queue')
|
||||
await TrackPlayer.setQueue([])
|
||||
setPlayQueue([])
|
||||
}
|
||||
|
||||
const addToQueue = async (tracks: JellifyTrack[]) => {
|
||||
const insertIndex = await findPlayQueueIndexStart(playQueue)
|
||||
console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`)
|
||||
|
||||
await TrackPlayer.add(tracks, insertIndex)
|
||||
|
||||
setPlayQueue((await getQueue()) as JellifyTrack[])
|
||||
|
||||
console.debug(`Queue has ${playQueue.length} tracks`)
|
||||
}
|
||||
|
||||
const addToNext = async (tracks: JellifyTrack[]) => {
|
||||
const insertIndex = await findPlayNextIndexStart(playQueue)
|
||||
|
||||
console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`)
|
||||
|
||||
await TrackPlayer.add(tracks, insertIndex)
|
||||
|
||||
setPlayQueue((await getQueue()) as JellifyTrack[])
|
||||
}
|
||||
//#endregion Functions
|
||||
|
||||
//#region Hooks
|
||||
const useAddToQueue = useMutation({
|
||||
mutationFn: async (mutation: AddToQueueMutation) => {
|
||||
trigger('impactLight')
|
||||
|
||||
if (mutation.queuingType === QueuingType.PlayingNext)
|
||||
return addToNext([
|
||||
mapDtoToTrack(mutation.track, downloadedTracks ?? [], mutation.queuingType),
|
||||
])
|
||||
else
|
||||
return addToQueue([
|
||||
mapDtoToTrack(mutation.track, downloadedTracks ?? [], mutation.queuingType),
|
||||
])
|
||||
},
|
||||
onSuccess: (data, { queuingType }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
Burnt.alert({
|
||||
title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
|
||||
duration: 1,
|
||||
preset: 'done',
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
const useRemoveFromQueue = useMutation({
|
||||
mutationFn: async (index: number) => {
|
||||
trigger('impactMedium')
|
||||
|
||||
await TrackPlayer.remove([index])
|
||||
|
||||
setPlayQueue((await TrackPlayer.getQueue()) as JellifyTrack[])
|
||||
},
|
||||
})
|
||||
|
||||
const useClearQueue = useMutation({
|
||||
mutationFn: async () => {
|
||||
trigger('effectDoubleClick')
|
||||
|
||||
await TrackPlayer.removeUpcomingTracks()
|
||||
|
||||
setPlayQueue((await getQueue()) as JellifyTrack[])
|
||||
},
|
||||
})
|
||||
|
||||
const useReorderQueue = useMutation({
|
||||
mutationFn: async (mutation: QueueOrderMutation) => {
|
||||
setPlayQueue(mutation.newOrder)
|
||||
await TrackPlayer.move(mutation.from, mutation.to)
|
||||
},
|
||||
const useStartPlayback = useMutation({
|
||||
mutationFn: play,
|
||||
})
|
||||
|
||||
const useTogglePlayback = useMutation({
|
||||
mutationFn: (index?: number | undefined) => {
|
||||
mutationFn: () => {
|
||||
trigger('impactMedium')
|
||||
if (playbackState === State.Playing) return pause()
|
||||
else return play(index)
|
||||
else return play()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -231,102 +80,6 @@ const PlayerContextInitializer = () => {
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const useSkip = useMutation({
|
||||
mutationFn: async (index?: number | undefined) => {
|
||||
trigger('impactMedium')
|
||||
|
||||
// Handle if this is the last track in the queue
|
||||
if (playQueue.length - 1 === (await getActiveTrackIndex())) return
|
||||
else {
|
||||
if (!isUndefined(index)) {
|
||||
setIsSkipping(true)
|
||||
setNowPlaying(playQueue[index])
|
||||
await skip(index)
|
||||
setIsSkipping(false)
|
||||
} else {
|
||||
const nowPlayingIndex = playQueue.findIndex(
|
||||
(track) => track.item.Id === nowPlaying!.item.Id,
|
||||
)
|
||||
setNowPlaying(playQueue[nowPlayingIndex + 1])
|
||||
await skipToNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const usePrevious = useMutation({
|
||||
mutationFn: async () => {
|
||||
trigger('impactMedium')
|
||||
|
||||
const nowPlayingIndex = playQueue.findIndex(
|
||||
(track) => track.item.Id === nowPlaying!.item.Id,
|
||||
)
|
||||
|
||||
const { position } = await TrackPlayer.getProgress()
|
||||
|
||||
if (nowPlayingIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) {
|
||||
setNowPlaying(playQueue[nowPlayingIndex - 1])
|
||||
await skipToPrevious()
|
||||
} else await seekTo(0)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* A mutation hook that adds tracks to the play queue
|
||||
*
|
||||
* Respects the network status of the app - if we are
|
||||
* online, tracks are always added to the queue for streaming
|
||||
* - if we are *offline* then only tracks that are downloaded
|
||||
* will be added to the queue
|
||||
*/
|
||||
const usePlayNewQueue = useMutation({
|
||||
mutationFn: async (mutation: QueueMutation) => {
|
||||
trigger('effectDoubleClick')
|
||||
|
||||
setIsSkipping(true)
|
||||
|
||||
// Optimistically set now playing
|
||||
|
||||
setNowPlaying(
|
||||
mapDtoToTrack(
|
||||
mutation.tracklist[mutation.index ?? 0],
|
||||
downloadedTracks ?? [],
|
||||
QueuingType.FromSelection,
|
||||
),
|
||||
)
|
||||
|
||||
await resetQueue(false)
|
||||
|
||||
const queueItems = networkStatusCheck(
|
||||
networkStatus,
|
||||
mutation.tracklist,
|
||||
downloadedTracks ?? [],
|
||||
)
|
||||
|
||||
console.debug(`Adding ${queueItems.length} to the queue`)
|
||||
|
||||
const queueTracks = queueItems.map((queueItem) => {
|
||||
return mapDtoToTrack(queueItem, downloadedTracks ?? [], QueuingType.FromSelection)
|
||||
})
|
||||
|
||||
console.debug(`Slotting ${queueTracks.length} track(s)`)
|
||||
|
||||
await addToQueue(queueTracks)
|
||||
|
||||
setQueue(mutation.queue)
|
||||
},
|
||||
onSuccess: async (data, mutation: QueueMutation) => {
|
||||
setIsSkipping(false)
|
||||
await play(mutation.index)
|
||||
|
||||
if (typeof mutation.queue === 'object') await markItemPlayed(queue as BaseItemDto)
|
||||
},
|
||||
onError: async () => {
|
||||
setIsSkipping(false)
|
||||
setNowPlaying((await TrackPlayer.getActiveTrack()) as JellifyTrack)
|
||||
},
|
||||
})
|
||||
//#endregion
|
||||
|
||||
//#region RNTP Setup
|
||||
@@ -384,7 +137,7 @@ const PlayerContextInitializer = () => {
|
||||
}
|
||||
|
||||
case Event.PlaybackActiveTrackChanged: {
|
||||
if (initialized && !isSkipping) {
|
||||
if (initialized) {
|
||||
const activeTrack = (await TrackPlayer.getActiveTrack()) as
|
||||
| JellifyTrack
|
||||
| undefined
|
||||
@@ -419,14 +172,6 @@ const PlayerContextInitializer = () => {
|
||||
//#endregion RNTP Setup
|
||||
|
||||
//#region useEffects
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.Queue, JSON.stringify(queue))
|
||||
}, [queue])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized && playQueue)
|
||||
storage.set(MMKVStorageKeys.PlayQueue, JSON.stringify(playQueue))
|
||||
}, [playQueue])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized && nowPlaying)
|
||||
@@ -435,15 +180,17 @@ const PlayerContextInitializer = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized && playQueue.length > 0 && nowPlaying) {
|
||||
TrackPlayer.setQueue(playQueue).then(() => {
|
||||
TrackPlayer.skip(
|
||||
playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!),
|
||||
)
|
||||
})
|
||||
TrackPlayer.skip(playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!))
|
||||
}
|
||||
|
||||
setInitialized(true)
|
||||
}, [playQueue, nowPlaying])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentIndex > -1 && playQueue.length > currentIndex)
|
||||
console.debug(`Setting now playing to queue index ${currentIndex}`)
|
||||
setNowPlaying(playQueue[currentIndex])
|
||||
}, [currentIndex])
|
||||
//#endregion useEffects
|
||||
|
||||
//#region return
|
||||
@@ -452,19 +199,9 @@ const PlayerContextInitializer = () => {
|
||||
nowPlayingIsFavorite,
|
||||
setNowPlayingIsFavorite,
|
||||
nowPlaying,
|
||||
playQueue,
|
||||
queue,
|
||||
getQueueSectionData,
|
||||
useAddToQueue,
|
||||
useClearQueue,
|
||||
setNowPlaying,
|
||||
useReorderQueue,
|
||||
useRemoveFromQueue,
|
||||
useStartPlayback,
|
||||
useTogglePlayback,
|
||||
useSeekTo,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
usePlayNewQueue,
|
||||
playbackState,
|
||||
}
|
||||
//#endregion return
|
||||
@@ -476,65 +213,7 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
nowPlayingIsFavorite: false,
|
||||
setNowPlayingIsFavorite: () => {},
|
||||
nowPlaying: undefined,
|
||||
setNowPlaying: () => {},
|
||||
playQueue: [],
|
||||
queue: 'Recently Played',
|
||||
getQueueSectionData: () => [],
|
||||
useAddToQueue: {
|
||||
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,
|
||||
},
|
||||
useClearQueue: {
|
||||
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,
|
||||
},
|
||||
useRemoveFromQueue: {
|
||||
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: {
|
||||
useStartPlayback: {
|
||||
mutate: () => {},
|
||||
mutateAsync: async () => {},
|
||||
data: undefined,
|
||||
@@ -588,61 +267,6 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
failureReason: null,
|
||||
submittedAt: 0,
|
||||
},
|
||||
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,
|
||||
},
|
||||
usePlayNewQueue: {
|
||||
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,
|
||||
},
|
||||
playbackState: undefined,
|
||||
})
|
||||
//#endregion Create PlayerContext
|
||||
|
||||
@@ -651,55 +275,9 @@ export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JS
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const {
|
||||
initialized,
|
||||
nowPlayingIsFavorite,
|
||||
setNowPlayingIsFavorite,
|
||||
nowPlaying,
|
||||
playQueue,
|
||||
queue,
|
||||
getQueueSectionData,
|
||||
useAddToQueue,
|
||||
useClearQueue,
|
||||
useRemoveFromQueue,
|
||||
useReorderQueue,
|
||||
useTogglePlayback,
|
||||
useSeekTo,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
setNowPlaying,
|
||||
usePlayNewQueue,
|
||||
playbackState,
|
||||
} = PlayerContextInitializer()
|
||||
const context = PlayerContextInitializer()
|
||||
|
||||
return (
|
||||
<QueueProvider>
|
||||
<PlayerContext.Provider
|
||||
value={{
|
||||
initialized,
|
||||
nowPlayingIsFavorite,
|
||||
setNowPlayingIsFavorite,
|
||||
nowPlaying,
|
||||
playQueue,
|
||||
queue,
|
||||
getQueueSectionData,
|
||||
useAddToQueue,
|
||||
useClearQueue,
|
||||
useRemoveFromQueue,
|
||||
useReorderQueue,
|
||||
useTogglePlayback,
|
||||
useSeekTo,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
setNowPlaying,
|
||||
usePlayNewQueue,
|
||||
playbackState,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PlayerContext.Provider>
|
||||
</QueueProvider>
|
||||
)
|
||||
return <PlayerContext.Provider value={context}>{children}</PlayerContext.Provider>
|
||||
}
|
||||
|
||||
export const usePlayerContext = () => useContext(PlayerContext)
|
||||
|
||||
@@ -1,20 +1,448 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
import React, { ReactNode, useContext, useEffect, useState } from 'react'
|
||||
import { createContext } from 'react'
|
||||
import { Queue } from './types/queue-item'
|
||||
import { Section } from '../components/Player/types'
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
|
||||
import { storage } from '../constants/storage'
|
||||
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'
|
||||
import { JellifyTrack } from '../types/JellifyTrack'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { mapDtoToTrack } from '../helpers/mappings'
|
||||
import { useNetworkContext } from '../components/Network/provider'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { findPlayQueueIndexStart } from './helpers'
|
||||
import { getQueue, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import * as Burnt from 'burnt'
|
||||
import { markItemPlayed } from '../api/mutations/functions/item'
|
||||
import { filterTracksOnNetworkStatus } from './helpers/queue'
|
||||
import { SKIP_TO_PREVIOUS_THRESHOLD } from './config'
|
||||
|
||||
interface QueueContext {}
|
||||
|
||||
const QueueContextInitailizer = () => {
|
||||
return {}
|
||||
interface QueueContext {
|
||||
queueRef: Queue
|
||||
playQueue: JellifyTrack[]
|
||||
currentIndex: number
|
||||
fetchQueueSectionData: () => Section[]
|
||||
useAddToQueue: UseMutationResult<void, Error, AddToQueueMutation, unknown>
|
||||
useLoadNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>
|
||||
useRemoveUpcomingTracks: UseMutationResult<void, Error, void, unknown>
|
||||
useRemoveFromQueue: UseMutationResult<void, Error, number, unknown>
|
||||
useReorderQueue: UseMutationResult<void, Error, QueueOrderMutation, unknown>
|
||||
useSkip: UseMutationResult<void, Error, number | undefined, unknown>
|
||||
usePrevious: UseMutationResult<void, Error, void, unknown>
|
||||
}
|
||||
|
||||
export const QueueContext = createContext<QueueContext>({})
|
||||
const QueueContextInitailizer = () => {
|
||||
const queueRefJson = storage.getString(MMKVStorageKeys.Queue)
|
||||
const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue)
|
||||
|
||||
const queueRefInit = queueRefJson ? JSON.parse(queueRefJson) : 'Recently Played'
|
||||
const playQueueInit = playQueueJson ? JSON.parse(playQueueJson) : []
|
||||
|
||||
const [queueRef, setQueueRef] = useState<Queue>(queueRefInit)
|
||||
const [playQueue, setPlayQueue] = useState<JellifyTrack[]>(playQueueInit)
|
||||
|
||||
const [updateRntp, setUpdateRntp] = useState<boolean>(true)
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(-1)
|
||||
|
||||
const { downloadedTracks, networkStatus } = useNetworkContext()
|
||||
|
||||
//#region Functions
|
||||
const fetchQueueSectionData: () => Section[] = () => {
|
||||
return Object.keys(QueuingType).map((type) => {
|
||||
return {
|
||||
title: type,
|
||||
data: playQueue.filter((track) => track.QueuingType === type),
|
||||
} as Section
|
||||
})
|
||||
}
|
||||
|
||||
const resetQueue = () => {
|
||||
console.debug(`Clearing queue of ${playQueue.length}`)
|
||||
setPlayQueue([])
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a {@link BaseItemDto} of a track on Jellyfin, and updates it's
|
||||
* position in the {@link queue}
|
||||
*
|
||||
*
|
||||
* @param track The Jellyfin track object to update and replace in the queue
|
||||
*/
|
||||
const replaceQueueItem: (track: BaseItemDto) => Promise<void> = async (track: BaseItemDto) => {
|
||||
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
|
||||
|
||||
const queueItemIndex = queue.findIndex((queuedTrack) => queuedTrack.item.Id === track.Id!)
|
||||
|
||||
// Update queued item at index if found, else silently do nothing
|
||||
if (queueItemIndex !== -1) {
|
||||
const queueItem = queue[queueItemIndex]
|
||||
|
||||
TrackPlayer.remove([queueItemIndex]).then(() => {
|
||||
TrackPlayer.add(
|
||||
mapDtoToTrack(track, downloadedTracks ?? [], queueItem.QueuingType),
|
||||
queueItemIndex,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const loadQueue = async (
|
||||
audioItems: BaseItemDto[],
|
||||
queuingRef: Queue,
|
||||
startIndex: number = 1,
|
||||
) => {
|
||||
console.debug(`Queuing ${audioItems.length} items`)
|
||||
|
||||
const availableAudioItems = filterTracksOnNetworkStatus(
|
||||
networkStatus,
|
||||
audioItems,
|
||||
downloadedTracks ?? [],
|
||||
)
|
||||
|
||||
console.debug(
|
||||
`Filtered out ${
|
||||
audioItems.length - availableAudioItems.length
|
||||
} due to network status being ${networkStatus}`,
|
||||
)
|
||||
|
||||
const queue = availableAudioItems.map((item) =>
|
||||
mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.FromSelection),
|
||||
)
|
||||
|
||||
setPlayQueue(queue)
|
||||
setQueueRef(queuingRef)
|
||||
setCurrentIndex(startIndex)
|
||||
|
||||
console.debug(`Queued ${queue.length} tracks, starting at ${startIndex}`)
|
||||
}
|
||||
|
||||
const playNextInQueue = async (item: BaseItemDto) => {
|
||||
console.debug(`Playing item next in queue`)
|
||||
|
||||
const playNextTrack = mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.PlayingNext)
|
||||
|
||||
setUpdateRntp(false)
|
||||
TrackPlayer.add([playNextTrack], currentIndex + 1)
|
||||
setPlayQueue([
|
||||
...playQueue.slice(0, currentIndex + 1),
|
||||
playNextTrack,
|
||||
...playQueue.slice(currentIndex + 1),
|
||||
])
|
||||
}
|
||||
|
||||
const playInQueue = async (items: BaseItemDto[]) => {
|
||||
const insertIndex = await findPlayQueueIndexStart(playQueue)
|
||||
console.debug(`Adding ${items.length} to queue at index ${insertIndex}`)
|
||||
|
||||
setUpdateRntp(false)
|
||||
await TrackPlayer.add(
|
||||
items.map((item) =>
|
||||
mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.DirectlyQueued),
|
||||
),
|
||||
insertIndex,
|
||||
)
|
||||
|
||||
setPlayQueue((await getQueue()) as JellifyTrack[])
|
||||
|
||||
console.debug(`Queue has ${playQueue.length} tracks`)
|
||||
}
|
||||
//#endregion Functions
|
||||
|
||||
//#region Hooks
|
||||
const useAddToQueue = useMutation({
|
||||
mutationFn: ({ track, queuingType }: AddToQueueMutation) => {
|
||||
return queuingType === QueuingType.PlayingNext
|
||||
? playNextInQueue(track)
|
||||
: playInQueue([track])
|
||||
},
|
||||
onSuccess: (data, { queuingType }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
Burnt.alert({
|
||||
title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
|
||||
duration: 0.5,
|
||||
preset: 'done',
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
const useLoadNewQueue = useMutation({
|
||||
mutationFn: async ({ index, track, tracklist, queuingType, queue }: QueueMutation) =>
|
||||
loadQueue(tracklist, queue, index),
|
||||
onSuccess: async (data, { queue }: QueueMutation) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
if (typeof queue === 'object') await markItemPlayed(queue)
|
||||
},
|
||||
})
|
||||
|
||||
const useRemoveFromQueue = useMutation({
|
||||
mutationFn: async (index: number) => {
|
||||
trigger('impactMedium')
|
||||
|
||||
setUpdateRntp(false)
|
||||
TrackPlayer.remove([index])
|
||||
setPlayQueue([
|
||||
...playQueue.slice(0, index),
|
||||
...playQueue.slice(index + 1, playQueue.length - 1),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const useRemoveUpcomingTracks = useMutation({
|
||||
mutationFn: async () => {
|
||||
setUpdateRntp(false)
|
||||
TrackPlayer.removeUpcomingTracks()
|
||||
setPlayQueue([...playQueue.slice(0, currentIndex + 1)])
|
||||
},
|
||||
onSuccess: () => {
|
||||
trigger('notificationSuccess')
|
||||
},
|
||||
})
|
||||
|
||||
const useReorderQueue = useMutation({
|
||||
mutationFn: async ({ from, to, newOrder }: QueueOrderMutation) => {
|
||||
setUpdateRntp(false)
|
||||
TrackPlayer.move(from, to)
|
||||
setPlayQueue(newOrder)
|
||||
},
|
||||
onSuccess: () => {
|
||||
trigger('notificationSuccess')
|
||||
},
|
||||
})
|
||||
|
||||
const useSkip = useMutation({
|
||||
mutationFn: async (index?: number | undefined) => {
|
||||
trigger('impactMedium')
|
||||
|
||||
console.debug(
|
||||
`Skip to next triggered. Index is ${`using ${
|
||||
index ? index : currentIndex
|
||||
} as index ${index ? 'since it was provided' : ''}`}`,
|
||||
)
|
||||
|
||||
if (index && index < playQueue.length - 1) setCurrentIndex(index)
|
||||
else if (playQueue.length - 1 > currentIndex) setCurrentIndex(currentIndex + 1)
|
||||
},
|
||||
})
|
||||
|
||||
const usePrevious = useMutation({
|
||||
mutationFn: async () => {
|
||||
trigger('impactMedium')
|
||||
|
||||
const { position } = await TrackPlayer.getProgress()
|
||||
|
||||
console.debug(`Skip to previous triggered. Index is ${currentIndex}`)
|
||||
|
||||
if (currentIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) {
|
||||
setCurrentIndex(currentIndex - 1)
|
||||
} else await seekTo(0)
|
||||
},
|
||||
})
|
||||
|
||||
const useUpdateRntpQueue = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (updateRntp) await TrackPlayer.setQueue(playQueue)
|
||||
|
||||
setUpdateRntp(true)
|
||||
},
|
||||
})
|
||||
|
||||
const useUpdateRntpIndex = useMutation({
|
||||
mutationFn: async (index: number) => {
|
||||
await TrackPlayer.skip(index)
|
||||
},
|
||||
})
|
||||
|
||||
//#endregion Hooks
|
||||
|
||||
//#region useEffect(s)
|
||||
/**
|
||||
* Update RNTP Queue when our queue
|
||||
* is updated
|
||||
*/
|
||||
useEffect(() => {
|
||||
useUpdateRntpQueue.mutate()
|
||||
storage.set(MMKVStorageKeys.PlayQueue, JSON.stringify(playQueue))
|
||||
}, [playQueue])
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.Queue, JSON.stringify(queueRef))
|
||||
}, [queueRef])
|
||||
|
||||
useEffect(() => {
|
||||
useUpdateRntpIndex.mutate(currentIndex)
|
||||
}, [currentIndex])
|
||||
|
||||
//#endregion useEffect(s)
|
||||
|
||||
return {
|
||||
queueRef,
|
||||
playQueue,
|
||||
currentIndex,
|
||||
setCurrentIndex,
|
||||
fetchQueueSectionData,
|
||||
useAddToQueue,
|
||||
useLoadNewQueue,
|
||||
useRemoveFromQueue,
|
||||
useRemoveUpcomingTracks,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
}
|
||||
}
|
||||
|
||||
export const QueueContext = createContext<QueueContext>({
|
||||
queueRef: 'Recently Played',
|
||||
playQueue: [],
|
||||
currentIndex: -1,
|
||||
fetchQueueSectionData: () => [],
|
||||
useAddToQueue: {
|
||||
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,
|
||||
},
|
||||
useLoadNewQueue: {
|
||||
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: {
|
||||
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,
|
||||
},
|
||||
useRemoveFromQueue: {
|
||||
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,
|
||||
},
|
||||
useRemoveUpcomingTracks: {
|
||||
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: {
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
export const QueueProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const urmo = QueueContextInitailizer()
|
||||
const context = QueueContextInitailizer()
|
||||
|
||||
return <QueueContext.Provider value={{}}>{children}</QueueContext.Provider>
|
||||
return <QueueContext.Provider value={context}>{children}</QueueContext.Provider>
|
||||
}
|
||||
|
||||
export const useQueueContext = () => useContext(QueueContext)
|
||||
|
||||
48
yarn.lock
48
yarn.lock
@@ -6539,17 +6539,6 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.4.2:
|
||||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
glob@^6.0.1:
|
||||
version "6.0.4"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz"
|
||||
integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==
|
||||
dependencies:
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "2 || 3"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
|
||||
@@ -6846,11 +6835,6 @@ ini@~1.3.0:
|
||||
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
insync@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/insync/-/insync-2.1.1.tgz"
|
||||
integrity sha512-UzUhOZFpCMM22Xlig9iUPqalf8n7c4eYScamce1C+jN3ad8FtmVm42ryMwVq0hAxHbwUhWFhPvTFQQpFdDUKkw==
|
||||
|
||||
internal-ip@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz"
|
||||
@@ -8478,13 +8462,6 @@ mimic-function@^5.0.0:
|
||||
resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz"
|
||||
integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==
|
||||
|
||||
"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz"
|
||||
@@ -8492,6 +8469,13 @@ minimatch@^10.0.1:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^8.0.2:
|
||||
version "8.0.4"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz"
|
||||
@@ -8610,11 +8594,6 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
ncp@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz"
|
||||
integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==
|
||||
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
|
||||
@@ -8684,17 +8663,6 @@ normalize-path@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
npm-bundle@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/npm-bundle/-/npm-bundle-3.0.3.tgz"
|
||||
integrity sha512-fHF7FR32YNgjqi0MQMLnE78Ff9/wYd4/7/Cke3dLLi2QzETKotIiWGCxwDoXAZDWVoTuVRYQa2ZdiZPuBL7QnA==
|
||||
dependencies:
|
||||
glob "^6.0.1"
|
||||
insync "^2.1.1"
|
||||
mkdirp "^0.5.1"
|
||||
ncp "^2.0.0"
|
||||
rimraf "^2.4.4"
|
||||
|
||||
npm-package-arg@^11.0.0:
|
||||
version "11.0.3"
|
||||
resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz"
|
||||
@@ -9911,7 +9879,7 @@ rfdc@^1.4.1:
|
||||
resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz"
|
||||
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
|
||||
|
||||
rimraf@^2.4.4, rimraf@^2.6.3:
|
||||
rimraf@^2.6.3:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
|
||||
Reference in New Issue
Block a user