mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-17 19:25:34 -06:00
Enhancments to Add to Playlist Sheet, Playback Reporting Bugfix (#623)
* some improvements to the addto playlist modal in terms of performance leveraging tanstack query cache for storing the tracks in a playlist eagerly fetching playlists on the home screen since we may want to display those someday anyways * fix playback reporting bug
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useFrequentlyPlayedArtists, useFrequentlyPlayedTracks } from '../frequents'
|
||||
import { useRecentArtists, useRecentlyPlayedTracks } from '../recents'
|
||||
import { useFrequentlyPlayedArtists, useFrequentlyPlayedTracks } from '../../queries/frequents'
|
||||
import { useRecentArtists, useRecentlyPlayedTracks } from '../../queries/recents'
|
||||
import { useUserPlaylists } from '../../queries/playlist'
|
||||
|
||||
const useHomeQueries = () => {
|
||||
const { refetch: refetchUserPlaylists } = useUserPlaylists()
|
||||
|
||||
const { refetch: refetchRecentArtists } = useRecentArtists()
|
||||
|
||||
const { refetch: refetchRecentlyPlayed } = useRecentlyPlayedTracks()
|
||||
@@ -13,7 +16,11 @@ const useHomeQueries = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await Promise.all([refetchRecentlyPlayed(), refetchFrequentlyPlayed()])
|
||||
await Promise.all([
|
||||
refetchRecentlyPlayed(),
|
||||
refetchFrequentlyPlayed(),
|
||||
refetchUserPlaylists(),
|
||||
])
|
||||
await Promise.all([refetchFrequentArtists(), refetchRecentArtists()])
|
||||
return true
|
||||
},
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { UserPlaylistsQueryKey } from './keys'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { fetchUserPlaylists, fetchPublicPlaylists } from './utils'
|
||||
import { ApiLimits } from '../query.config'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export const useUserPlaylists = () => {
|
||||
const { api, user, library } = useJellifyContext()
|
||||
@@ -17,3 +20,20 @@ export const useUserPlaylists = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const usePlaylistTracks = (playlist: BaseItemDto) => {
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
|
||||
queryFn: () => {
|
||||
return getItemsApi(api!)
|
||||
.getItems({
|
||||
parentId: playlist.Id!,
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : []
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { addManyToPlaylist, addToPlaylist } from '../../api/mutations/playlists'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useMemo } from 'react'
|
||||
import { useState } from 'react'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { YStack, XStack, Spacer, YGroup, Separator, ListItem, getTokens, ScrollView } from 'tamagui'
|
||||
import Icon from '../Global/components/icon'
|
||||
@@ -16,8 +13,9 @@ import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../Player/component.config'
|
||||
import { getItemName } from '../../utils/text'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
import { useUserPlaylists } from '../../api/queries/playlist'
|
||||
import { usePlaylistTracks, useUserPlaylists } from '../../api/queries/playlist'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
|
||||
export default function AddToPlaylist({
|
||||
track,
|
||||
@@ -28,96 +26,14 @@ export default function AddToPlaylist({
|
||||
tracks?: BaseItemDto[]
|
||||
source?: BaseItemDto
|
||||
}): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const { bottom } = useSafeAreaInsets()
|
||||
|
||||
const {
|
||||
data: playlists,
|
||||
refetch,
|
||||
isPending: playlistsFetchPending,
|
||||
isSuccess: playlistsFetchSuccess,
|
||||
} = useUserPlaylists()
|
||||
|
||||
// Fetch all playlist tracks to check if the current track(s) is/are already in any playlists
|
||||
const playlistsWithTracks = useQuery({
|
||||
queryKey: [QueryKeys.PlaylistItemCheckCache, playlists?.map((p) => p.Id).join(',')],
|
||||
enabled: !!playlists && playlists.length > 0,
|
||||
queryFn: () => {
|
||||
console.debug('Fetching playlist contents')
|
||||
return Promise.all(
|
||||
playlists!.map(async (playlist) => {
|
||||
const response = await getItemsApi(api!).getItems({
|
||||
parentId: playlist.Id!,
|
||||
})
|
||||
return {
|
||||
playlistId: playlist.Id!,
|
||||
tracks: response.data.Items || [],
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Check if a track is in a playlist
|
||||
const isTrackInPlaylist = useMemo(() => {
|
||||
if (!playlistsWithTracks.data) return {}
|
||||
const selectedTracks = tracks ?? (track ? [track] : [])
|
||||
const result: Record<string, boolean> = {}
|
||||
playlistsWithTracks.data.forEach((playlistData) => {
|
||||
result[playlistData.playlistId] = selectedTracks.length
|
||||
? selectedTracks.every((t) =>
|
||||
playlistData.tracks.some((playlistTrack) => playlistTrack.Id === t.Id),
|
||||
)
|
||||
: false
|
||||
})
|
||||
return result
|
||||
}, [playlistsWithTracks.data, track?.Id, tracks?.length])
|
||||
|
||||
const useAddToPlaylist = useMutation({
|
||||
mutationFn: ({
|
||||
track,
|
||||
playlist,
|
||||
tracks,
|
||||
}: AddToPlaylistMutation & { tracks?: BaseItemDto[] }) => {
|
||||
trigger('impactLight')
|
||||
if (tracks && tracks.length > 0) {
|
||||
return addManyToPlaylist(api, user, tracks, playlist)
|
||||
}
|
||||
|
||||
return addToPlaylist(api, user, track!, playlist)
|
||||
},
|
||||
onSuccess: (data, { playlist }) => {
|
||||
Toast.show({
|
||||
text1: 'Added to playlist',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
trigger('notificationSuccess')
|
||||
|
||||
if (refetch) void refetch()
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
|
||||
})
|
||||
|
||||
// Invalidate our playlist check cache
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.PlaylistItemCheckCache],
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show({
|
||||
text1: 'Unable to add',
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
{(source ?? track) && (
|
||||
@@ -134,7 +50,7 @@ export default function AddToPlaylist({
|
||||
{(source ?? track)?.ArtistItems && (
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold>
|
||||
{`${(source ?? track)!.ArtistItems?.map((artist) => getItemName(artist)).join(',')}`}
|
||||
{`${(source ?? track)!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
)}
|
||||
@@ -144,54 +60,110 @@ export default function AddToPlaylist({
|
||||
|
||||
{!playlistsFetchPending && playlistsFetchSuccess && (
|
||||
<YGroup separator={<Separator />} marginBottom={bottom} paddingBottom={'$10'}>
|
||||
{playlists?.map((playlist) => {
|
||||
const isInPlaylist = isTrackInPlaylist[playlist.Id!]
|
||||
|
||||
return (
|
||||
<YGroup.Item key={playlist.Id!}>
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
disabled={isInPlaylist}
|
||||
hoverTheme
|
||||
opacity={isInPlaylist ? 0.7 : 1}
|
||||
pressStyle={{ opacity: 0.5 }}
|
||||
onPress={() => {
|
||||
if (!isInPlaylist) {
|
||||
useAddToPlaylist.mutate({
|
||||
track,
|
||||
tracks,
|
||||
playlist,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<XStack alignItems='center' gap={'$2'}>
|
||||
<ItemImage item={playlist} height={'$11'} width={'$11'} />
|
||||
|
||||
<YStack alignItems='flex-start' flex={5}>
|
||||
<Text bold>{playlist.Name ?? 'Untitled Playlist'}</Text>
|
||||
|
||||
<Text color={getTokens().color.amethyst.val}>{`${
|
||||
playlist.ChildCount ?? 0
|
||||
} tracks`}</Text>
|
||||
</YStack>
|
||||
|
||||
{isInPlaylist ? (
|
||||
<Icon
|
||||
flex={1}
|
||||
name='check-circle-outline'
|
||||
color={'$success'}
|
||||
/>
|
||||
) : (
|
||||
<Spacer flex={1} />
|
||||
)}
|
||||
</XStack>
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
)
|
||||
})}
|
||||
{playlists?.map((playlist) => (
|
||||
<AddToPlaylistRow
|
||||
key={playlist.Id}
|
||||
playlist={playlist}
|
||||
tracks={tracks ? tracks : track ? [track] : []}
|
||||
/>
|
||||
))}
|
||||
</YGroup>
|
||||
)}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
function AddToPlaylistRow({
|
||||
playlist,
|
||||
tracks,
|
||||
}: {
|
||||
playlist: BaseItemDto
|
||||
tracks: BaseItemDto[]
|
||||
}): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const {
|
||||
data: playlistTracks,
|
||||
isPending: fetchingPlaylistTracks,
|
||||
refetch: refetchPlaylistTracks,
|
||||
} = usePlaylistTracks(playlist)
|
||||
|
||||
const useAddToPlaylist = useMutation({
|
||||
mutationFn: ({
|
||||
track,
|
||||
playlist,
|
||||
tracks,
|
||||
}: AddToPlaylistMutation & { tracks?: BaseItemDto[] }) => {
|
||||
trigger('impactLight')
|
||||
if (tracks && tracks.length > 0) {
|
||||
return addManyToPlaylist(api, user, tracks, playlist)
|
||||
}
|
||||
|
||||
return addToPlaylist(api, user, track!, playlist)
|
||||
},
|
||||
onSuccess: (data, { playlist }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
setIsInPlaylist(true)
|
||||
|
||||
refetchPlaylistTracks()
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show({
|
||||
text1: 'Unable to add',
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
const [isInPlaylist, setIsInPlaylist] = useState<boolean>(
|
||||
tracks.filter((track) =>
|
||||
playlistTracks?.map((playlistTrack) => playlistTrack.Id).includes(track.Id),
|
||||
).length > 0,
|
||||
)
|
||||
|
||||
return (
|
||||
<YGroup.Item key={playlist.Id!}>
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
disabled={isInPlaylist}
|
||||
hoverTheme
|
||||
opacity={isInPlaylist ? 0.7 : 1}
|
||||
pressStyle={{ opacity: 0.5 }}
|
||||
onPress={() => {
|
||||
if (!isInPlaylist) {
|
||||
useAddToPlaylist.mutate({
|
||||
track: undefined,
|
||||
tracks,
|
||||
playlist,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<XStack alignItems='center' gap={'$2'}>
|
||||
<ItemImage item={playlist} height={'$11'} width={'$11'} />
|
||||
|
||||
<YStack alignItems='flex-start' flex={5}>
|
||||
<Text bold>{playlist.Name ?? 'Untitled Playlist'}</Text>
|
||||
|
||||
<Text color={getTokens().color.amethyst.val}>{`${
|
||||
playlistTracks?.length ?? 0
|
||||
} tracks`}</Text>
|
||||
</YStack>
|
||||
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
{isInPlaylist ? (
|
||||
<Icon flex={1} name='check-circle-outline' color={'$success'} />
|
||||
) : (
|
||||
<Spacer flex={1} />
|
||||
)}
|
||||
</Animated.View>
|
||||
</XStack>
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import RecentlyPlayed from './helpers/recently-played'
|
||||
import FrequentArtists from './helpers/frequent-artists'
|
||||
import FrequentlyPlayedTracks from './helpers/frequent-tracks'
|
||||
import { usePreventRemove } from '@react-navigation/native'
|
||||
import useHomeQueries from '../../api/queries/home'
|
||||
import useHomeQueries from '../../api/mutations/home'
|
||||
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
|
||||
|
||||
const COMPONENT_NAME = 'Home'
|
||||
|
||||
@@ -5,10 +5,8 @@ import { AddToQueueMutation, QueueMutation } from '../interfaces'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { shuffleJellifyTracks } from '../utils/shuffle'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { findPlayQueueIndexStart } from '../utils'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { getActiveIndex, getCurrentTrack } from '.'
|
||||
import { getCurrentTrack } from '.'
|
||||
import { JellifyDownload } from '../../../types/JellifyDownload'
|
||||
import { usePlayerQueueStore } from '../../../stores/player/queue'
|
||||
|
||||
@@ -79,7 +77,13 @@ export async function loadQueue({
|
||||
|
||||
console.debug(`Final start index is ${finalStartIndex}`)
|
||||
|
||||
await TrackPlayer.setQueue(queue)
|
||||
/**
|
||||
* Keep the requested track as the currently playing track so there
|
||||
* isn't any flickering in the miniplayer
|
||||
*/
|
||||
await TrackPlayer.setQueue([queue[finalStartIndex]])
|
||||
await TrackPlayer.add([...queue.slice(0, finalStartIndex), ...queue.slice(finalStartIndex + 1)])
|
||||
await TrackPlayer.move(0, finalStartIndex)
|
||||
|
||||
console.debug(
|
||||
`Queued ${queue.length} tracks, starting at ${finalStartIndex}${shuffled ? ' (shuffled)' : ''}`,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { useDownloadingDeviceProfile } from '../../stores/device-profile'
|
||||
import { NOW_PLAYING_QUERY } from './constants/queries'
|
||||
import Initialize from './functions/initialization'
|
||||
import { useEnableAudioNormalization } from '../../stores/settings/player'
|
||||
import { useCurrentIndex, usePlayerQueueStore } from '../../stores/player/queue'
|
||||
|
||||
const PLAYER_EVENTS: Event[] = [
|
||||
Event.PlaybackActiveTrackChanged,
|
||||
@@ -42,8 +41,6 @@ export const PlayerProvider: () => React.JSX.Element = () => {
|
||||
|
||||
usePerformanceMonitor('PlayerProvider', 3)
|
||||
|
||||
const currentIndex = useCurrentIndex()
|
||||
|
||||
const isRestoring = useIsRestoring()
|
||||
|
||||
const eventHandler = useCallback(
|
||||
@@ -57,29 +54,22 @@ export const PlayerProvider: () => React.JSX.Element = () => {
|
||||
// When we load a new queue, our index is updated before RNTP
|
||||
// Because of this, we only need to respond to this event
|
||||
// if the index from the event differs from what we have stored
|
||||
if (event.index && event.index !== currentIndex) {
|
||||
if (event.track && enableAudioNormalization) {
|
||||
console.debug('Normalizing audio track')
|
||||
nowPlaying = event.track as JellifyTrack
|
||||
if (event.track && enableAudioNormalization) {
|
||||
console.debug('Normalizing audio track')
|
||||
nowPlaying = event.track as JellifyTrack
|
||||
|
||||
const volume = calculateTrackVolume(nowPlaying)
|
||||
await TrackPlayer.setVolume(volume)
|
||||
} else if (event.track) {
|
||||
reportPlaybackStarted(api, event.track)
|
||||
}
|
||||
const volume = calculateTrackVolume(nowPlaying)
|
||||
await TrackPlayer.setVolume(volume)
|
||||
} else if (event.track) {
|
||||
reportPlaybackStarted(api, event.track)
|
||||
}
|
||||
|
||||
await handleActiveTrackChanged()
|
||||
await handleActiveTrackChanged()
|
||||
|
||||
if (event.lastTrack) {
|
||||
if (
|
||||
isPlaybackFinished(
|
||||
event.lastPosition,
|
||||
event.lastTrack.duration ?? 1,
|
||||
)
|
||||
)
|
||||
reportPlaybackCompleted(api, event.lastTrack as JellifyTrack)
|
||||
else reportPlaybackStopped(api, event.lastTrack as JellifyTrack)
|
||||
}
|
||||
if (event.lastTrack) {
|
||||
if (isPlaybackFinished(event.lastPosition, event.lastTrack.duration ?? 1))
|
||||
reportPlaybackCompleted(api, event.lastTrack as JellifyTrack)
|
||||
else reportPlaybackStopped(api, event.lastTrack as JellifyTrack)
|
||||
}
|
||||
break
|
||||
|
||||
@@ -112,7 +102,7 @@ export const PlayerProvider: () => React.JSX.Element = () => {
|
||||
break
|
||||
}
|
||||
},
|
||||
[api, autoDownload, enableAudioNormalization, currentIndex],
|
||||
[api, autoDownload, enableAudioNormalization],
|
||||
)
|
||||
|
||||
useTrackPlayerEvents(PLAYER_EVENTS, eventHandler)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useMutation, UseMutationResult, useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query'
|
||||
import { useJellifyContext } from '..'
|
||||
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { removeFromPlaylist, updatePlaylist } from '../../api/mutations/playlists'
|
||||
import { RemoveFromPlaylistMutation } from '../../components/Playlist/interfaces'
|
||||
import { SharedValue, useSharedValue } from 'react-native-reanimated'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
import { usePlaylistTracks } from '../../api/queries/playlist'
|
||||
|
||||
interface PlaylistContext {
|
||||
playlist: BaseItemDto
|
||||
@@ -42,23 +41,7 @@ const PlaylistContextInitializer = (playlist: BaseItemDto) => {
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const {
|
||||
data: tracks,
|
||||
isPending,
|
||||
refetch,
|
||||
isSuccess,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
|
||||
queryFn: () => {
|
||||
return getItemsApi(api!)
|
||||
.getItems({
|
||||
parentId: playlist.Id!,
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : []
|
||||
})
|
||||
},
|
||||
})
|
||||
const { data: tracks, isPending, refetch, isSuccess } = usePlaylistTracks(playlist)
|
||||
|
||||
const useUpdatePlaylist = useMutation({
|
||||
mutationFn: ({ playlist, tracks }: { playlist: BaseItemDto; tracks: BaseItemDto[] }) => {
|
||||
|
||||
Reference in New Issue
Block a user