mirror of
https://github.com/Jellify-Music/App.git
synced 2026-04-29 06:59:30 -05:00
Merge branch 'main' of github.com:Jellify-Music/App
This commit is contained in:
@@ -68,19 +68,15 @@ export function AlbumScreen({ route, navigation }: HomeAlbumProps): React.JSX.El
|
||||
const allTracks = discs.flatMap((disc) => disc.data)
|
||||
if (allTracks.length === 0) return
|
||||
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: album,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: album,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
startPlayback: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,6 @@ import { StackParamList } from '../types'
|
||||
import React from 'react'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { useQueueContext } from '../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../providers/Player'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { fetchAlbumDiscs } from '../../api/queries/item'
|
||||
|
||||
@@ -26,7 +25,6 @@ export default function ArtistTabBar(
|
||||
const { api } = useJellifyContext()
|
||||
const { artist, scroll, albums } = useArtistContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
@@ -47,19 +45,15 @@ export default function ArtistTabBar(
|
||||
|
||||
if (allTracks.length === 0) return
|
||||
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: artist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: allTracks[0],
|
||||
index: 0,
|
||||
tracklist: allTracks,
|
||||
queue: artist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
startPlayback: true,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to play artist tracks:', error)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Text } from '../helpers/text'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
interface CardProps extends TamaguiCardProps {
|
||||
caption?: string | null | undefined
|
||||
@@ -25,14 +22,7 @@ interface CardProps extends TamaguiCardProps {
|
||||
* @param props
|
||||
*/
|
||||
export function ItemCard(props: CardProps) {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const mediaInfo = useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, props.item.Id!],
|
||||
queryFn: () => fetchMediaInfo(api, user, props.item),
|
||||
staleTime: Infinity,
|
||||
enabled: props.item.Type === 'Audio',
|
||||
})
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
return (
|
||||
<View alignItems='center' margin={'$1.5'}>
|
||||
|
||||
@@ -37,24 +37,19 @@ export default function ItemRow({
|
||||
onPress?: () => void
|
||||
circular?: boolean
|
||||
}): React.JSX.Element {
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
|
||||
const gestureCallback = () => {
|
||||
switch (item.Type) {
|
||||
case 'Audio': {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
index: 0,
|
||||
queue: 'Search',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
index: 0,
|
||||
queue: 'Search',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -16,7 +16,6 @@ import { networkStatusTypes } from '../../../components/Network/internetConnecti
|
||||
import { useNetworkContext } from '../../../providers/Network'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { fetchItem } from '../../../api/queries/item'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
@@ -67,22 +66,6 @@ export default function Track({
|
||||
|
||||
const isOffline = networkStatus === networkStatusTypes.DISCONNECTED
|
||||
|
||||
// Fetch media info so it's available in the player
|
||||
const mediaInfo = useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, track.Id!],
|
||||
queryFn: () => fetchMediaInfo(api, user, track),
|
||||
staleTime: Infinity,
|
||||
enabled: track.Type === 'Audio',
|
||||
})
|
||||
|
||||
// Fetch album so it's available in the Details screen
|
||||
const { data: album } = useQuery({
|
||||
queryKey: [QueryKeys.Item, track.Id!], // Different key
|
||||
queryFn: () => fetchItem(api, track.Id!),
|
||||
staleTime: 60 * 60 * 1000 * 24, // 24 hours
|
||||
enabled: !!track.Id, // Add proper enabled condition
|
||||
})
|
||||
|
||||
return (
|
||||
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
|
||||
<XStack
|
||||
@@ -95,18 +78,14 @@ export default function Track({
|
||||
if (onPress) {
|
||||
onPress()
|
||||
} else {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
onLongPress={
|
||||
|
||||
@@ -8,7 +8,6 @@ import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { H4 } from '../../../components/Global/helpers/text'
|
||||
import { useDisplayContext } from '../../../providers/Display/display-provider'
|
||||
export default function FrequentlyPlayedTracks({
|
||||
@@ -23,7 +22,6 @@ export default function FrequentlyPlayedTracks({
|
||||
isFetchingFrequentlyPlayed,
|
||||
} = useHomeContext()
|
||||
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const { horizontalItems } = useDisplayContext()
|
||||
|
||||
@@ -58,20 +56,16 @@ export default function FrequentlyPlayedTracks({
|
||||
subCaption={`${track.Artists?.join(', ')}`}
|
||||
squared
|
||||
onPress={() => {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
useLoadNewQueue({
|
||||
track,
|
||||
index,
|
||||
tracklist: frequentlyPlayed?.pages.flatMap((page) => page) ?? [
|
||||
track,
|
||||
index,
|
||||
tracklist: frequentlyPlayed?.pages.flatMap((page) => page) ?? [
|
||||
track,
|
||||
],
|
||||
queue: 'On Repeat',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
],
|
||||
queue: 'On Repeat',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function RecentlyPlayed({
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { nowPlaying, useStartPlayback } = usePlayerContext()
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
|
||||
@@ -59,20 +59,16 @@ export default function RecentlyPlayed({
|
||||
testId={`recently-played-${index}`}
|
||||
item={recentlyPlayedTrack}
|
||||
onPress={() => {
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks?.pages.flatMap((page) => page) ?? [
|
||||
recentlyPlayedTrack,
|
||||
],
|
||||
queue: 'Recently Played',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks?.pages.flatMap((page) => page) ?? [
|
||||
recentlyPlayedTrack,
|
||||
],
|
||||
queue: 'Recently Played',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
startPlayback: true,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Scrubber(): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
// Get progress from the track player with the specified update interval
|
||||
const progress = useProgress(UPDATE_INTERVAL, false)
|
||||
const { position, duration } = useProgress(UPDATE_INTERVAL)
|
||||
|
||||
// Single source of truth for the current position
|
||||
const [displayPosition, setDisplayPosition] = useState<number>(0)
|
||||
@@ -33,16 +33,13 @@ export default function Scrubber(): React.JSX.Element {
|
||||
|
||||
// Calculate maximum track duration in slider units
|
||||
const maxDuration = useMemo(() => {
|
||||
return progress?.duration
|
||||
? Math.round(progress.duration * ProgressMultiplier)
|
||||
: ProgressMultiplier
|
||||
}, [progress?.duration])
|
||||
return Math.round(duration * ProgressMultiplier)
|
||||
}, [duration])
|
||||
|
||||
// Calculate current position in slider units
|
||||
const calculatedPosition = useMemo(() => {
|
||||
if (!progress?.position) return 0
|
||||
return Math.round(progress.position * ProgressMultiplier)
|
||||
}, [progress?.position])
|
||||
return Math.round(position * ProgressMultiplier)
|
||||
}, [position])
|
||||
|
||||
// Update display position from playback progress
|
||||
useEffect(() => {
|
||||
@@ -93,8 +90,8 @@ export default function Scrubber(): React.JSX.Element {
|
||||
|
||||
// Get total duration in seconds
|
||||
const totalSeconds = useMemo(() => {
|
||||
return progress?.duration ? Math.round(progress.duration) : 0
|
||||
}, [progress?.duration])
|
||||
return Math.round(duration)
|
||||
}, [duration])
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={scrubGesture}>
|
||||
|
||||
@@ -10,6 +10,7 @@ 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)
|
||||
|
||||
@@ -95,7 +96,8 @@ export default function Queue({
|
||||
showArtwork
|
||||
testID={`queue-item-${getIndex()}`}
|
||||
onPress={() => {
|
||||
useSkip.mutate(getIndex())
|
||||
const index = getIndex()
|
||||
if (!isUndefined(index)) useSkip.mutate(index)
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactLight')
|
||||
@@ -104,7 +106,8 @@ export default function Queue({
|
||||
isNested
|
||||
showRemove
|
||||
onRemove={() => {
|
||||
if (getIndex()) useRemoveFromQueue.mutate(getIndex()!)
|
||||
const index = getIndex()
|
||||
if (!isUndefined(index)) useRemoveFromQueue.mutate(index)
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
|
||||
@@ -17,7 +17,6 @@ import { useSettingsContext } from '../../../../src/providers/Settings'
|
||||
import { ActivityIndicator } from 'react-native'
|
||||
import { mapDtoToTrack } from '../../../utils/mappings'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
|
||||
export default function PlayliistTracklistHeader(
|
||||
@@ -150,7 +149,6 @@ function PlaylistHeaderControls({
|
||||
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
|
||||
const { downloadQuality, streamingQuality } = useSettingsContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const { useStartPlayback } = usePlayerContext()
|
||||
const isDownloading = pendingDownloads.length != 0
|
||||
const { sessionId, api } = useJellifyContext()
|
||||
|
||||
@@ -165,19 +163,15 @@ function PlaylistHeaderControls({
|
||||
const playPlaylist = (shuffled: boolean = false) => {
|
||||
if (!playlistTracks || playlistTracks.length === 0) return
|
||||
|
||||
useLoadNewQueue.mutate(
|
||||
{
|
||||
track: playlistTracks[0],
|
||||
index: 0,
|
||||
tracklist: playlistTracks,
|
||||
queue: playlist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
},
|
||||
{
|
||||
onSuccess: () => useStartPlayback.mutate(),
|
||||
},
|
||||
)
|
||||
useLoadNewQueue({
|
||||
track: playlistTracks[0],
|
||||
index: 0,
|
||||
tracklist: playlistTracks,
|
||||
queue: playlist,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
shuffled,
|
||||
startPlayback: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function InfoTabIndex({ navigation }: InfoTabNativeStackNavigatio
|
||||
title: `Jellify`,
|
||||
subTitle: version,
|
||||
iconName: 'jellyfish',
|
||||
iconColor: '$borderColor',
|
||||
iconColor: '$secondary',
|
||||
children: (
|
||||
<YStack gap={'$2'}>
|
||||
<Text
|
||||
|
||||
@@ -14,22 +14,11 @@ export default function PlaybackTab(): React.JSX.Element {
|
||||
return (
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Gapless Playback',
|
||||
subTitle: 'Seamless transitions between tracks',
|
||||
iconName: 'skip-next',
|
||||
iconColor: '$borderColor',
|
||||
children: (
|
||||
<Text fontSize='$3' color='$color10' padding='$3'>
|
||||
Gapless playback is automatically enabled for smooth music transitions.
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Streaming Quality',
|
||||
subTitle: `Current: ${getQualityLabel(streamingQuality)} • ${getBandwidthEstimate(streamingQuality)}`,
|
||||
iconName: 'wifi',
|
||||
iconColor: '$primary',
|
||||
iconName: 'sine-wave',
|
||||
iconColor: getStreamingQualityIconColor(streamingQuality),
|
||||
children: (
|
||||
<YStack gap='$2' paddingVertical='$2'>
|
||||
<Text bold fontSize='$4'>
|
||||
@@ -72,3 +61,18 @@ export default function PlaybackTab(): React.JSX.Element {
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function getStreamingQualityIconColor(streamingQuality: StreamingQuality): string {
|
||||
switch (streamingQuality) {
|
||||
case 'original':
|
||||
return '$success'
|
||||
case 'high':
|
||||
return '$success'
|
||||
case 'medium':
|
||||
return '$secondary'
|
||||
case 'low':
|
||||
return '$danger'
|
||||
default:
|
||||
return '$borderColor'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ interface PlayerContext {
|
||||
shuffled: boolean
|
||||
useToggleRepeatMode: UseMutationResult<void, Error, void, unknown>
|
||||
useToggleShuffle: UseMutationResult<void, Error, void, unknown>
|
||||
useStartPlayback: UseMutationResult<void, Error, void, unknown>
|
||||
useStartPlayback: () => void
|
||||
useTogglePlayback: UseMutationResult<void, Error, void, unknown>
|
||||
useSeekTo: UseMutationResult<void, Error, number, unknown>
|
||||
useSeekBy: UseMutationResult<void, Error, number, unknown>
|
||||
@@ -358,7 +358,7 @@ const PlayerContextInitializer = () => {
|
||||
/**
|
||||
* A mutation to handle starting playback
|
||||
*/
|
||||
const useStartPlayback = useMutation({
|
||||
const { mutate: useStartPlayback } = useMutation({
|
||||
mutationFn: TrackPlayer.play,
|
||||
})
|
||||
|
||||
@@ -661,24 +661,7 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
submittedAt: 0,
|
||||
},
|
||||
playbackState: undefined,
|
||||
useStartPlayback: {
|
||||
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,
|
||||
},
|
||||
useStartPlayback: () => {},
|
||||
useTogglePlayback: {
|
||||
mutate: () => {},
|
||||
mutateAsync: async () => {},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import JellifyTrack from '../types/JellifyTrack'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Queue } from './types/queue-item'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
|
||||
/**
|
||||
* A mutation to handle loading a new queue.
|
||||
@@ -34,6 +33,11 @@ export interface QueueMutation {
|
||||
* Whether the queue should be shuffled.
|
||||
*/
|
||||
shuffled?: boolean | undefined
|
||||
|
||||
/**
|
||||
* Whether to start playback immediately.
|
||||
*/
|
||||
startPlayback?: boolean | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3,7 +3,7 @@ import { createContext } from 'react'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
import { Section } from '../../components/Player/types'
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from '../../player/interfaces'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
|
||||
import { storage } from '../../constants/storage'
|
||||
import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
|
||||
import JellifyTrack from '../../types/JellifyTrack'
|
||||
@@ -14,7 +14,6 @@ import { useSettingsContext } from '../Settings'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import TrackPlayer, { Event, useTrackPlayerEvents } from 'react-native-track-player'
|
||||
import { findPlayQueueIndexStart } from './utils'
|
||||
import { play, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
|
||||
|
||||
@@ -27,6 +26,7 @@ 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'
|
||||
|
||||
/**
|
||||
* @description The context for managing the queue
|
||||
@@ -76,7 +76,7 @@ interface QueueContext {
|
||||
/**
|
||||
* A hook that loads a new queue of tracks
|
||||
*/
|
||||
useLoadNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>
|
||||
useLoadNewQueue: (mutation: QueueMutation) => void
|
||||
|
||||
/**
|
||||
* A hook that removes upcoming tracks from the queue
|
||||
@@ -192,17 +192,10 @@ const QueueContextInitailizer = () => {
|
||||
|
||||
if (itemIndex !== -1) {
|
||||
newIndex = itemIndex
|
||||
console.debug(`Active track changed to index ${itemIndex}`)
|
||||
console.debug(`Active track changed to item at index: ${itemIndex}`)
|
||||
|
||||
// Ensure upcoming tracks are in correct order (important for shuffle)
|
||||
// try {
|
||||
// const { ensureUpcomingTracksInQueue } = await import(
|
||||
// '../../player/helpers/gapless'
|
||||
// )
|
||||
// await ensureUpcomingTracksInQueue(playQueue, index)
|
||||
// } catch (error) {
|
||||
// console.debug('Failed to ensure upcoming tracks on track change:', error)
|
||||
// }
|
||||
await ensureUpcomingTracksInQueue(playQueue, itemIndex)
|
||||
} else {
|
||||
console.warn('No index found for active track')
|
||||
}
|
||||
@@ -356,7 +349,7 @@ const QueueContextInitailizer = () => {
|
||||
`Queued ${queue.length} tracks, starting at ${finalStartIndex}${shuffleQueue ? ' (shuffled)' : ''}`,
|
||||
)
|
||||
|
||||
await play()
|
||||
await TrackPlayer.play()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,7 +443,7 @@ const QueueContextInitailizer = () => {
|
||||
|
||||
if (currentIndex > 0 && Math.floor(position) < SKIP_TO_PREVIOUS_THRESHOLD) {
|
||||
TrackPlayer.skipToPrevious()
|
||||
} else await seekTo(0)
|
||||
} else await TrackPlayer.seekTo(0)
|
||||
}
|
||||
|
||||
const skip = async (index?: number | undefined) => {
|
||||
@@ -530,7 +523,7 @@ const QueueContextInitailizer = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const useLoadNewQueue = useMutation({
|
||||
const { mutate: useLoadNewQueue } = useMutation({
|
||||
mutationFn: async ({
|
||||
index,
|
||||
track,
|
||||
@@ -539,9 +532,11 @@ const QueueContextInitailizer = () => {
|
||||
queue,
|
||||
shuffled,
|
||||
}: QueueMutation) => loadQueue(tracklist, queue, index, shuffled),
|
||||
onSuccess: async (data, { queue }: QueueMutation) => {
|
||||
onSuccess: async (data, { queue, startPlayback }: QueueMutation) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
startPlayback && (await TrackPlayer.play())
|
||||
|
||||
if (typeof queue === 'object' && api && user) await markItemPlayed(api, user, queue)
|
||||
},
|
||||
})
|
||||
@@ -734,24 +729,7 @@ export const QueueContext = createContext<QueueContext>({
|
||||
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,
|
||||
},
|
||||
useLoadNewQueue: () => {},
|
||||
useSkip: {
|
||||
mutate: () => {},
|
||||
mutateAsync: async () => {},
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { isEmpty } from 'lodash'
|
||||
import { isEmpty, isUndefined } from 'lodash'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { getActiveTrackIndex } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
|
||||
/**
|
||||
* Finds and returns the index of the player queue to insert additional tracks into
|
||||
* @param playQueue The current player queue
|
||||
* @returns The index to insert songs to play next at
|
||||
*/
|
||||
export const findPlayNextIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
export async function findPlayNextIndexStart(playQueue: JellifyTrack[]) {
|
||||
if (isEmpty(playQueue)) return 0
|
||||
|
||||
return (await getActiveTrackIndex())! + 1
|
||||
const activeTrack = (await TrackPlayer.getActiveTrack()) as JellifyTrack
|
||||
|
||||
const activeIndex = playQueue.findIndex((track) => track.item.Id === activeTrack?.item.Id)
|
||||
|
||||
if (isUndefined(activeTrack) || activeIndex === -1) return 0
|
||||
else return activeIndex + 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,16 +24,20 @@ export const findPlayNextIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
* @param playQueue The current player queue
|
||||
* @returns The index to insert songs to add to the user queue
|
||||
*/
|
||||
export const findPlayQueueIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
export async function findPlayQueueIndexStart(playQueue: JellifyTrack[]) {
|
||||
if (isEmpty(playQueue)) return 0
|
||||
|
||||
const activeIndex = await getActiveTrackIndex()
|
||||
const activeTrack = (await TrackPlayer.getActiveTrack()) as JellifyTrack
|
||||
|
||||
if (playQueue.findIndex((track) => track.QueuingType === QueuingType.FromSelection) === -1)
|
||||
return activeIndex! + 1
|
||||
const activeIndex = playQueue.findIndex((track) => track.item.Id === activeTrack?.item.Id)
|
||||
|
||||
return playQueue.findIndex(
|
||||
(queuedTrack, index) =>
|
||||
queuedTrack.QueuingType === QueuingType.FromSelection && index > activeIndex!,
|
||||
if (isUndefined(activeTrack) || activeIndex === -1) return 0
|
||||
|
||||
const insertIndex = playQueue.findIndex(
|
||||
({ QueuingType: queuingType, index }) =>
|
||||
queuingType === QueuingType.FromSelection && index > activeIndex,
|
||||
)
|
||||
|
||||
if (insertIndex === -1) return playQueue.length
|
||||
else return insertIndex
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export function Tabs({
|
||||
name='music-box-multiple'
|
||||
color={color}
|
||||
size={size}
|
||||
testID='library-tab-icon'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -106,7 +107,12 @@ export function Tabs({
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<MaterialCommunityIcons name='earth' color={color} size={size} />
|
||||
<MaterialCommunityIcons
|
||||
name='earth'
|
||||
color={color}
|
||||
size={size}
|
||||
testID='discover-tab-icon'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
+22
-15
@@ -4,16 +4,16 @@ import {
|
||||
PlaybackInfoResponse,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import JellifyTrack from '../types/JellifyTrack'
|
||||
import { RatingType, TrackType } from 'react-native-track-player'
|
||||
import { TrackType } from 'react-native-track-player'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { JellifyDownload } from '../types/JellifyDownload'
|
||||
import { queryClient } from '../constants/query-client'
|
||||
import { QueryKeys } from '../enums/query-keys'
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
import RNFS from 'react-native-fs'
|
||||
import { DownloadQuality, StreamingQuality } from '../providers/Settings'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
/**
|
||||
* The container that the Jellyfin server will attempt to transcode to
|
||||
@@ -25,6 +25,16 @@ import { DownloadQuality, StreamingQuality } from '../providers/Settings'
|
||||
*/
|
||||
const transcodingContainer = 'ts'
|
||||
|
||||
/**
|
||||
* The type of track to use for the player
|
||||
*
|
||||
* iOS can use HLS, Android can't - and therefore uses Default
|
||||
*
|
||||
* Why? I'm not sure - someone way smarter than me can probably explain it
|
||||
* - Violet Caulfield - 2025-07-20
|
||||
*/
|
||||
const type = Platform.OS === 'ios' ? TrackType.HLS : TrackType.Default
|
||||
|
||||
/**
|
||||
* Gets quality-specific parameters for transcoding
|
||||
*
|
||||
@@ -97,15 +107,16 @@ export function mapDtoToTrack(
|
||||
console.debug(
|
||||
`Mapping BaseItemDTO to Track object with streaming quality: ${qualityForStreaming}`,
|
||||
)
|
||||
const isFavorite = !isUndefined(item.UserData) && (item.UserData.IsFavorite ?? false)
|
||||
|
||||
const downloads = downloadedTracks.filter((download) => download.item.Id === item.Id)
|
||||
|
||||
let url: string
|
||||
let image: string | undefined
|
||||
|
||||
if (downloads.length > 0 && downloads[0].path)
|
||||
if (downloads.length > 0 && downloads[0].path) {
|
||||
url = `file://${RNFS.DocumentDirectoryPath}/${downloads[0].path.split('/').pop()}`
|
||||
else {
|
||||
image = `file://${RNFS.DocumentDirectoryPath}/${downloads[0].artwork?.split('/').pop()}`
|
||||
} else {
|
||||
const PlaybackInfoResponse = queryClient.getQueryData([
|
||||
QueryKeys.MediaSources,
|
||||
item.Id!,
|
||||
@@ -118,12 +129,15 @@ export function mapDtoToTrack(
|
||||
)
|
||||
url = PlaybackInfoResponse.MediaSources![0].TranscodingUrl
|
||||
else url = `${api.basePath}/Audio/${item.Id!}/universal?${new URLSearchParams(urlParams)}`
|
||||
|
||||
image = item.AlbumId
|
||||
? getImageApi(api).getItemImageUrlById(item.AlbumId, ImageType.Primary)
|
||||
: undefined
|
||||
}
|
||||
|
||||
console.debug(url.length)
|
||||
return {
|
||||
url,
|
||||
type: TrackType.Default,
|
||||
type,
|
||||
headers: {
|
||||
'X-Emby-Token': api.accessToken,
|
||||
},
|
||||
@@ -131,14 +145,7 @@ export function mapDtoToTrack(
|
||||
album: item.Album,
|
||||
artist: item.Artists?.join(', '),
|
||||
duration: item.RunTimeTicks,
|
||||
artwork: item.AlbumId
|
||||
? getImageApi(api).getItemImageUrlById(item.AlbumId, ImageType.Primary, {
|
||||
width: 300,
|
||||
height: 300,
|
||||
})
|
||||
: undefined,
|
||||
|
||||
rating: isFavorite ? RatingType.Heart : undefined,
|
||||
artwork: image,
|
||||
item,
|
||||
QueuingType: queuingType ?? QueuingType.DirectlyQueued,
|
||||
} as JellifyTrack
|
||||
|
||||
Reference in New Issue
Block a user