mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-30 02:09:41 -06:00
Queue Provider Optimizations, Alphabetical Selector Optimizations, Artist Pagination Improvements (#467)
* make now playing change faster when loading a new queue * queue provider optimizations * Improvements to the alphabetical selector handling in the artists page. * Improvements to artists pagination - fetching more artists at a given time
This commit is contained in:
@@ -5,14 +5,23 @@ import { Event } from 'react-native-track-player'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Button, Text } from 'react-native'
|
||||
|
||||
import { QueueProvider, useQueueContext } from '../../src/providers/Player/queue'
|
||||
import {
|
||||
QueueProvider,
|
||||
useCurrentIndexContext,
|
||||
usePreviousContext,
|
||||
useSetPlayQueueContext,
|
||||
useSkipContext,
|
||||
} from '../../src/providers/Player/queue'
|
||||
import { eventHandler } from '../setup/rntp'
|
||||
import JellifyTrack from '../../src/types/JellifyTrack'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const QueueConsumer = () => {
|
||||
const { currentIndex, useSkip, usePrevious, playQueue, setPlayQueue } = useQueueContext()
|
||||
const currentIndex = useCurrentIndexContext()
|
||||
const useSkip = useSkipContext()
|
||||
const usePrevious = usePreviousContext()
|
||||
const setPlayQueue = useSetPlayQueueContext()
|
||||
|
||||
const tracklist: JellifyTrack[] = [
|
||||
{
|
||||
|
||||
@@ -5,7 +5,16 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Button, Text } from 'react-native'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
import { QueueProvider, useQueueContext } from '../../src/providers/Player/queue'
|
||||
import {
|
||||
QueueProvider,
|
||||
usePlayQueueContext,
|
||||
useCurrentIndexContext,
|
||||
useSetPlayQueueContext,
|
||||
useSetCurrentIndexContext,
|
||||
useShuffledContext,
|
||||
useSetUnshuffledQueueContext,
|
||||
useUnshuffledQueueContext,
|
||||
} from '../../src/providers/Player/queue'
|
||||
import { PlayerProvider, usePlayerContext } from '../../src/providers/Player'
|
||||
import JellifyTrack from '../../src/types/JellifyTrack'
|
||||
import { QueuingType } from '../../src/enums/queuing-type'
|
||||
@@ -82,15 +91,13 @@ const createMockTracks = (count: number): JellifyTrack[] => {
|
||||
}
|
||||
|
||||
const TestComponent = () => {
|
||||
const {
|
||||
playQueue,
|
||||
setPlayQueue,
|
||||
currentIndex,
|
||||
setCurrentIndex,
|
||||
shuffled,
|
||||
unshuffledQueue,
|
||||
setUnshuffledQueue,
|
||||
} = useQueueContext()
|
||||
const playQueue = usePlayQueueContext()
|
||||
const setPlayQueue = useSetPlayQueueContext()
|
||||
const currentIndex = useCurrentIndexContext()
|
||||
const setCurrentIndex = useSetCurrentIndexContext()
|
||||
const shuffled = useShuffledContext()
|
||||
const unshuffledQueue = useUnshuffledQueueContext()
|
||||
const setUnshuffledQueue = useSetUnshuffledQueueContext()
|
||||
const { useToggleShuffle, shuffled: playerShuffled } = usePlayerContext()
|
||||
|
||||
const testTracks = createMockTracks(5)
|
||||
|
||||
14
package.json
14
package.json
@@ -39,10 +39,10 @@
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||
"@react-native-picker/picker": "^2.11.1",
|
||||
"@react-navigation/bottom-tabs": "^7.4.4",
|
||||
"@react-navigation/material-top-tabs": "^7.3.4",
|
||||
"@react-navigation/native": "^7.1.16",
|
||||
"@react-navigation/native-stack": "^7.3.23",
|
||||
"@react-navigation/bottom-tabs": "^7.4.5",
|
||||
"@react-navigation/material-top-tabs": "^7.3.5",
|
||||
"@react-navigation/native": "^7.1.17",
|
||||
"@react-navigation/native-stack": "^7.3.24",
|
||||
"@sentry/react-native": "^6.17.0",
|
||||
"@shopify/flash-list": "^2.0.1",
|
||||
"@tamagui/config": "^1.132.15",
|
||||
@@ -89,7 +89,9 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"ruby": "^0.6.1",
|
||||
"tamagui": "^1.132.15"
|
||||
"scheduler": "^0.26.0",
|
||||
"tamagui": "^1.132.15",
|
||||
"use-context-selector": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.0",
|
||||
@@ -137,4 +139,4 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,43 +3,45 @@ import { Api } from '@jellyfin/sdk/lib/api'
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
ItemFields,
|
||||
ItemSortBy,
|
||||
SortOrder,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
import QueryConfig from './query.config'
|
||||
import { alphabet } from '../../providers/Library'
|
||||
|
||||
export function fetchArtists(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
page: string | number,
|
||||
page: number,
|
||||
isFavorite: boolean | undefined,
|
||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
): Promise<{ title: string | number; data: BaseItemDto[] }> {
|
||||
): Promise<BaseItemDto[]> {
|
||||
console.debug('Fetching artists', page)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!api) return reject('No API instance provided')
|
||||
if (!user) return reject('No user provided')
|
||||
if (!library) return reject('Library has not been set')
|
||||
|
||||
getArtistsApi(api)
|
||||
.getAlbumArtists({
|
||||
parentId: library?.musicLibraryId,
|
||||
userId: user?.id,
|
||||
parentId: library.musicLibraryId,
|
||||
userId: user.id,
|
||||
enableUserData: true,
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
startIndex: typeof page === 'number' ? page * QueryConfig.limits.library : 0,
|
||||
startIndex: page * QueryConfig.limits.library,
|
||||
limit: QueryConfig.limits.library,
|
||||
isFavorite: isFavorite,
|
||||
nameStartsWith: typeof page === 'string' && page !== alphabet[0] ? page : undefined,
|
||||
nameLessThan: typeof page === 'string' && page === alphabet[0] ? 'A' : undefined,
|
||||
fields: [ItemFields.SortName],
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items
|
||||
? resolve({ title: page, data: response.data.Items })
|
||||
: resolve({ title: page, data: [] })
|
||||
console.debug('Artists Response received')
|
||||
return response.data.Items ? resolve(response.data.Items) : resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
|
||||
@@ -17,7 +17,7 @@ import Icon from '../Global/components/icon'
|
||||
import { mapDtoToTrack } from '../../utils/mappings'
|
||||
import { useNetworkContext } from '../../providers/Network'
|
||||
import { useSettingsContext } from '../../providers/Settings'
|
||||
import { useQueueContext } from '../../providers/Player/queue'
|
||||
import { useLoadQueueContext } from '../../providers/Player/queue'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { useAlbumContext } from '../../providers/Album'
|
||||
|
||||
@@ -41,7 +41,7 @@ export function Album({ navigation }: AlbumProps): React.JSX.Element {
|
||||
failedDownloads,
|
||||
} = useNetworkContext()
|
||||
const { downloadQuality, streamingQuality } = useSettingsContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
|
||||
const downloadAlbum = (item: BaseItemDto[]) => {
|
||||
if (!api || !sessionId) return
|
||||
|
||||
@@ -14,7 +14,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import React from 'react'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { useQueueContext } from '../../providers/Player/queue'
|
||||
import { useLoadQueueContext } from '../../providers/Player/queue'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { fetchAlbumDiscs } from '../../api/queries/item'
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function ArtistTabBar(
|
||||
) {
|
||||
const { api } = useJellifyContext()
|
||||
const { artist, scroll, albums } = useArtistContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
|
||||
@@ -1,62 +1,96 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { getToken, getTokenValue, Separator, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { getToken, Separator, useTheme, XStack, YStack, Spinner } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import { ActivityIndicator, RefreshControl } from 'react-native'
|
||||
import { ArtistsProps } from '../types'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import { useLibraryContext, useLibrarySortAndFilterContext } from '../../providers/Library'
|
||||
import { useLibrarySortAndFilterContext } from '../../providers/Library'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
||||
import { AZScroller } from '../Global/components/alphabetical-selector'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { isString } from 'lodash'
|
||||
|
||||
/**
|
||||
* @param artistsInfiniteQuery - The infinite query for artists
|
||||
* @param navigation - The navigation object
|
||||
* @param showAlphabeticalSelector - Whether to show the alphabetical selector
|
||||
* @param artistPageParams - The page params for the artists - which are the A-Z letters that have been seen
|
||||
* @returns The Artists component
|
||||
*/
|
||||
export default function Artists({
|
||||
artistsInfiniteQuery,
|
||||
navigation,
|
||||
showAlphabeticalSelector,
|
||||
artistPageParams,
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
const { artistPageParams } = useLibraryContext()
|
||||
const theme = useTheme()
|
||||
const { isFavorites } = useLibrarySortAndFilterContext()
|
||||
|
||||
const artists = artistsInfiniteQuery.data ?? []
|
||||
const sectionListRef = useRef<FlashListRef<string | number | BaseItemDto>>(null)
|
||||
|
||||
const itemHeight = getToken('$6')
|
||||
const pendingLetterRef = useRef<string | null>(null)
|
||||
|
||||
const MemoizedItem = React.memo(ItemRow)
|
||||
|
||||
const artistsRef = useRef<(string | number | BaseItemDto)[]>(artistsInfiniteQuery.data ?? [])
|
||||
|
||||
const alphabeticalSelectorCallback = async (letter: string) => {
|
||||
console.debug(`Alphabetical Selector Callback: ${letter}`)
|
||||
|
||||
do {
|
||||
if (artistPageParams.current.includes(letter)) break
|
||||
await artistsInfiniteQuery.fetchNextPage({ cancelRefetch: true })
|
||||
} while (
|
||||
!artistsRef.current.includes(letter) &&
|
||||
artistsInfiniteQuery.hasNextPage &&
|
||||
(!artistsInfiniteQuery.isFetchNextPageError || artistsInfiniteQuery.isFetchingNextPage)
|
||||
)
|
||||
while (
|
||||
!artistPageParams!.current.has(letter.toUpperCase()) &&
|
||||
artistsInfiniteQuery.hasNextPage
|
||||
) {
|
||||
if (!artistsInfiniteQuery.isPending) {
|
||||
await artistsInfiniteQuery.fetchNextPage()
|
||||
}
|
||||
}
|
||||
console.debug(`Alphabetical Selector Callback: ${letter} complete`)
|
||||
}
|
||||
|
||||
const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } = useMutation({
|
||||
mutationFn: (letter: string) => alphabeticalSelectorCallback(letter),
|
||||
onSuccess: (data, letter) => {
|
||||
setTimeout(() => {
|
||||
sectionListRef.current?.scrollToIndex({
|
||||
index: artistsRef.current!.indexOf(letter),
|
||||
viewPosition: 0.1,
|
||||
animated: true,
|
||||
})
|
||||
}, 500)
|
||||
pendingLetterRef.current = letter.toUpperCase()
|
||||
},
|
||||
})
|
||||
|
||||
// Effect for handling the pending alphabet selector letter
|
||||
useEffect(() => {
|
||||
artistsRef.current = artistsInfiniteQuery.data ?? []
|
||||
console.debug(`artists: ${JSON.stringify(artistsInfiniteQuery.data)}`)
|
||||
}, [artistsInfiniteQuery.data])
|
||||
if (isString(pendingLetterRef.current) && artistsInfiniteQuery.data) {
|
||||
const upperLetters = artists
|
||||
.filter((item): item is string => typeof item === 'string')
|
||||
.map((letter) => letter.toUpperCase())
|
||||
.sort()
|
||||
|
||||
const index = upperLetters.findIndex((letter) => letter >= pendingLetterRef.current!)
|
||||
|
||||
if (index !== -1) {
|
||||
const letterToScroll = upperLetters[index]
|
||||
const scrollIndex = artists.indexOf(letterToScroll)
|
||||
if (scrollIndex !== -1) {
|
||||
sectionListRef.current?.scrollToIndex({
|
||||
index: scrollIndex,
|
||||
viewPosition: 0.1,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// fallback: scroll to last section
|
||||
const lastLetter = upperLetters[upperLetters.length - 1]
|
||||
const scrollIndex = artists.indexOf(lastLetter)
|
||||
if (scrollIndex !== -1) {
|
||||
sectionListRef.current?.scrollToIndex({
|
||||
index: scrollIndex,
|
||||
viewPosition: 0.1,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pendingLetterRef.current = null
|
||||
}
|
||||
}, [pendingLetterRef.current, artistsInfiniteQuery.data])
|
||||
|
||||
return (
|
||||
<XStack flex={1}>
|
||||
@@ -79,20 +113,20 @@ export default function Artists({
|
||||
: item.Id!
|
||||
}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
data={artistsInfiniteQuery.data}
|
||||
data={artists}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
colors={[theme.primary.val]}
|
||||
refreshing={artistsInfiniteQuery.isPending || isAlphabetSelectorPending}
|
||||
progressViewOffset={getTokenValue('$10')}
|
||||
refreshing={artistsInfiniteQuery.isFetching || isAlphabetSelectorPending}
|
||||
onRefresh={() => artistsInfiniteQuery.refetch()}
|
||||
tintColor={theme.primary.val}
|
||||
/>
|
||||
}
|
||||
renderItem={({ index, item: artist }) =>
|
||||
typeof artist === 'string' ? (
|
||||
// Don't render the letter if we don't have any artists that start with it
|
||||
// If the index is the last index, or the next index is not an object, then don't render the letter
|
||||
index - 1 === artistsInfiniteQuery.data!.length ||
|
||||
typeof artistsInfiniteQuery.data![index + 1] !== 'object' ? null : (
|
||||
index - 1 === artists.length ||
|
||||
typeof artists[index + 1] !== 'object' ? null : (
|
||||
<XStack
|
||||
padding={'$2'}
|
||||
backgroundColor={'$background'}
|
||||
@@ -114,17 +148,9 @@ export default function Artists({
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
artistsInfiniteQuery.isPending ||
|
||||
artistsInfiniteQuery.isFetchingNextPage ? null : (
|
||||
<YStack justifyContent='center'>
|
||||
<Text>No artists</Text>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
stickyHeaderIndices={
|
||||
showAlphabeticalSelector
|
||||
? artistsInfiniteQuery.data
|
||||
? artists
|
||||
?.map((artist, index, artists) =>
|
||||
typeof artist === 'string' ? index : 0,
|
||||
)
|
||||
@@ -142,7 +168,9 @@ export default function Artists({
|
||||
removeClippedSubviews
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && <AZScroller onLetterSelect={alphabetSelectorMutate} />}
|
||||
{showAlphabeticalSelector && artistPageParams && (
|
||||
<AZScroller onLetterSelect={alphabetSelectorMutate} />
|
||||
)}
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { fetchUserPlaylists } from '../../../api/queries/playlists'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { getImageApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useNetworkContext } from '../../../providers/Network'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { useAddToQueueContext } from '../../../providers/Player/queue'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import Icon from '../../../components/Global/components/icon'
|
||||
@@ -111,7 +111,7 @@ export default function TrackOptions({
|
||||
return result
|
||||
}, [playlistsWithTracks.data, track.Id])
|
||||
|
||||
const { useAddToQueue } = useQueueContext()
|
||||
const useAddToQueue = useAddToQueueContext()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { getTokenValue, Token, useTheme } from 'tamagui'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { ImageStyle, StyleProp, ViewStyle } from 'react-native'
|
||||
import { ImageStyle } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
interface ImageProps {
|
||||
item: BaseItemDto
|
||||
@@ -28,7 +29,10 @@ export default function ItemImage({
|
||||
|
||||
const imageUrl =
|
||||
api &&
|
||||
((item.AlbumId && getImageApi(api).getItemImageUrlById(item.AlbumId)) ||
|
||||
((item.AlbumId &&
|
||||
getImageApi(api).getItemImageUrlById(item.AlbumId, ImageType.Primary, {
|
||||
tag: item.ImageTags?.Primary,
|
||||
})) ||
|
||||
(item.Id && getImageApi(api).getItemImageUrlById(item.Id)) ||
|
||||
'')
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Text } from '../helpers/text'
|
||||
import Icon from './icon'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { useLoadQueueContext } from '../../../providers/Player/queue'
|
||||
import ItemImage from './image'
|
||||
import FavoriteIcon from './favorite-icon'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
@@ -36,7 +36,7 @@ export default function ItemRow({
|
||||
onPress?: () => void
|
||||
circular?: boolean
|
||||
}): React.JSX.Element {
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
|
||||
const gestureCallback = () => {
|
||||
switch (item.Type) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
|
||||
import { useNetworkContext } from '../../../providers/Network'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { useLoadQueueContext, usePlayQueueContext } from '../../../providers/Player/queue'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import DownloadedIcon from './downloaded-icon'
|
||||
|
||||
@@ -53,7 +53,8 @@ export default function Track({
|
||||
const theme = useTheme()
|
||||
const { api } = useJellifyContext()
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
const { playQueue, useLoadNewQueue } = useQueueContext()
|
||||
const playQueue = usePlayQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
const { downloadedTracks, networkStatus } = useNetworkContext()
|
||||
|
||||
const isPlaying = nowPlaying?.item.Id === track.Id
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ItemCard } from '../../../components/Global/components/item-card'
|
||||
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 { useLoadQueueContext } from '../../../providers/Player/queue'
|
||||
import { H4 } from '../../../components/Global/helpers/text'
|
||||
import { useDisplayContext } from '../../../providers/Display/display-provider'
|
||||
export default function FrequentlyPlayedTracks({
|
||||
@@ -22,7 +22,7 @@ export default function FrequentlyPlayedTracks({
|
||||
isFetchingFrequentlyPlayed,
|
||||
} = useHomeContext()
|
||||
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
const { horizontalItems } = useDisplayContext()
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import { trigger } from 'react-native-haptic-feedback'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { useLoadQueueContext } from '../../../providers/Player/queue'
|
||||
import { useDisplayContext } from '../../../providers/Display/display-provider'
|
||||
|
||||
export default function RecentlyPlayed({
|
||||
@@ -20,7 +20,7 @@ export default function RecentlyPlayed({
|
||||
}): React.JSX.Element {
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
|
||||
const { recentTracks, fetchNextRecentTracks, hasNextRecentTracks, isFetchingRecentTracks } =
|
||||
useHomeContext()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../types'
|
||||
|
||||
export default function ArtistsTab(): React.JSX.Element {
|
||||
const { artistsInfiniteQuery } = useLibraryContext()
|
||||
const { artistsInfiniteQuery, artistPageParams } = useLibraryContext()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function ArtistsTab(): React.JSX.Element {
|
||||
artistsInfiniteQuery={artistsInfiniteQuery}
|
||||
navigation={navigation}
|
||||
showAlphabeticalSelector={true}
|
||||
artistPageParams={artistPageParams}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,19 @@ import { Spacer, XStack, getToken } from 'tamagui'
|
||||
import PlayPauseButton from './buttons'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import {
|
||||
usePreviousContext,
|
||||
useShuffledContext,
|
||||
useSkipContext,
|
||||
} from '../../../providers/Player/queue'
|
||||
import { RepeatMode } from 'react-native-track-player'
|
||||
|
||||
export default function Controls(): React.JSX.Element {
|
||||
const { usePrevious, useSkip } = useQueueContext()
|
||||
const usePrevious = usePreviousContext()
|
||||
const useSkip = useSkipContext()
|
||||
const { useToggleShuffle, useToggleRepeatMode, repeatMode } = usePlayerContext()
|
||||
|
||||
const { shuffled } = useQueueContext()
|
||||
const shuffled = useShuffledContext()
|
||||
|
||||
return (
|
||||
<XStack
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { useQueueRefContext } from '../../../providers/Player/queue'
|
||||
import { getToken, useWindowDimensions, XStack, YStack, useTheme } from 'tamagui'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
@@ -21,7 +21,7 @@ export default function PlayerHeader({
|
||||
|
||||
const isPlaying = playbackState === State.Playing
|
||||
|
||||
const { queueRef } = useQueueContext()
|
||||
const queueRef = useQueueRefContext()
|
||||
|
||||
const { width } = useWindowDimensions()
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import PlayPauseButton from './components/buttons'
|
||||
import { ProgressMultiplier, TextTickerConfig } from './component.config'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useQueueContext } from '../../providers/Player/queue'
|
||||
import { usePreviousContext, useSkipContext } from '../../providers/Player/queue'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { RunTimeSeconds } from '../Global/helpers/time-codes'
|
||||
import { UPDATE_INTERVAL } from '../../player/config'
|
||||
@@ -23,7 +23,8 @@ export const Miniplayer = React.memo(function Miniplayer({
|
||||
}): React.JSX.Element {
|
||||
const { api } = useJellifyContext()
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
const { useSkip, usePrevious } = useQueueContext()
|
||||
const useSkip = useSkipContext()
|
||||
const usePrevious = usePreviousContext()
|
||||
// Get progress from the track player with the specified update interval
|
||||
const progress = useProgress(UPDATE_INTERVAL)
|
||||
|
||||
|
||||
@@ -5,7 +5,14 @@ import { usePlayerContext } from '../../providers/Player'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import DraggableFlatList from 'react-native-draggable-flatlist'
|
||||
import { Separator, XStack } from 'tamagui'
|
||||
import { useQueueContext } from '../../providers/Player/queue'
|
||||
import {
|
||||
useRemoveUpcomingTracksContext,
|
||||
useRemoveFromQueueContext,
|
||||
useReorderQueueContext,
|
||||
useSkipContext,
|
||||
useQueueRefContext,
|
||||
usePlayQueueContext,
|
||||
} from '../../providers/Player/queue'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useLayoutEffect } from 'react'
|
||||
@@ -17,14 +24,12 @@ export default function Queue({
|
||||
}): React.JSX.Element {
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
|
||||
const {
|
||||
playQueue,
|
||||
queueRef,
|
||||
useRemoveUpcomingTracks,
|
||||
useRemoveFromQueue,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
} = useQueueContext()
|
||||
const playQueue = usePlayQueueContext()
|
||||
const queueRef = useQueueRefContext()
|
||||
const useRemoveUpcomingTracks = useRemoveUpcomingTracksContext()
|
||||
const useRemoveFromQueue = useRemoveFromQueueContext()
|
||||
const useReorderQueue = useReorderQueueContext()
|
||||
const useSkip = useSkipContext()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useNetworkContext } from '../../../../src/providers/Network'
|
||||
import { useSettingsContext } from '../../../../src/providers/Settings'
|
||||
import { ActivityIndicator } from 'react-native'
|
||||
import { mapDtoToTrack } from '../../../utils/mappings'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { useLoadQueueContext } from '../../../providers/Player/queue'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
|
||||
export default function PlayliistTracklistHeader(
|
||||
@@ -148,7 +148,7 @@ function PlaylistHeaderControls({
|
||||
}): React.JSX.Element {
|
||||
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
|
||||
const { downloadQuality, streamingQuality } = useSettingsContext()
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
const isDownloading = pendingDownloads.length != 0
|
||||
const { sessionId, api } = useJellifyContext()
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ export default function LabsTab(): React.JSX.Element {
|
||||
children: (
|
||||
<Button
|
||||
onPress={() => {
|
||||
storage.delete(QueryKeys.AllArtistsAlphabetical)
|
||||
storage.delete(QueryKeys.InfiniteArtists)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.AllArtistsAlphabetical],
|
||||
queryKey: [QueryKeys.InfiniteArtists],
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
3
src/components/types.d.ts
vendored
3
src/components/types.d.ts
vendored
@@ -8,6 +8,8 @@ import {
|
||||
InfiniteQueryObserverResult,
|
||||
UseInfiniteQueryResult,
|
||||
} from '@tanstack/react-query'
|
||||
import { RefObject } from 'react'
|
||||
|
||||
export type StackParamList = {
|
||||
Login: {
|
||||
screen: keyof StackParamList
|
||||
@@ -169,6 +171,7 @@ export type ArtistsProps = {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
showAlphabeticalSelector: boolean
|
||||
artistPageParams?: RefObject<Set<string>>
|
||||
}
|
||||
export type AlbumsProps = {
|
||||
albums: (string | number | BaseItemDto)[] | undefined
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { LibraryProvider } from '../providers/Library'
|
||||
|
||||
/**
|
||||
* An enum of all the keys of query functions.
|
||||
*/
|
||||
@@ -86,8 +88,27 @@ export enum QueryKeys {
|
||||
AllAlbums = 'AllAlbums',
|
||||
StorageInUse = 'StorageInUse',
|
||||
Patrons = 'Patrons',
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link InfiniteArtists} instead
|
||||
*/
|
||||
AllArtistsAlphabetical = 'AllArtistsAlphabetical',
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link InfiniteAlbums} instead after refactoring
|
||||
* the infinite query in the {@link LibraryProvider}
|
||||
*/
|
||||
AllAlbumsAlphabetical = 'AllAlbumsAlphabetical',
|
||||
RecentlyAddedAlbums = 'RecentlyAddedAlbums',
|
||||
PublicPlaylists = 'PublicPlaylists',
|
||||
|
||||
/**
|
||||
* Query representing the fetching of artists in an infinite query
|
||||
*/
|
||||
InfiniteArtists = 'InfiniteArtists',
|
||||
|
||||
/**
|
||||
* Query representing the fetching of albums in an infinite query
|
||||
*/
|
||||
InfiniteAlbums = 'InfiniteAlbums',
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@ import { createContext, useEffect, useState } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { CarPlay } from 'react-native-carplay'
|
||||
import { useJellifyContext } from '../index'
|
||||
import { useQueueContext } from '../Player/queue'
|
||||
import { useLoadQueueContext } from '../Player/queue'
|
||||
|
||||
interface CarPlayContext {
|
||||
carplayConnected: boolean
|
||||
}
|
||||
|
||||
const CarPlayContextInitializer = () => {
|
||||
const { user, api, sessionId } = useJellifyContext()
|
||||
const { user, api } = useJellifyContext()
|
||||
const [carplayConnected, setCarPlayConnected] = useState(CarPlay ? CarPlay.connected : false)
|
||||
const { useLoadNewQueue } = useQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
|
||||
useEffect(() => {
|
||||
function onConnect() {
|
||||
|
||||
@@ -15,6 +15,8 @@ import { fetchTracks } from '../../api/queries/tracks'
|
||||
import { fetchAlbums } from '../../api/queries/album'
|
||||
import { useLibrarySortAndFilterContext } from './sorting-filtering'
|
||||
import { fetchUserPlaylists } from '../../api/queries/playlists'
|
||||
import Artists from '../../components/Artists/component'
|
||||
import { isString, isUndefined } from 'lodash'
|
||||
|
||||
export const alphabet = '#abcdefghijklmnopqrstuvwxyz'.split('')
|
||||
|
||||
@@ -47,7 +49,7 @@ interface LibraryContext {
|
||||
isPendingAlbums: boolean
|
||||
isPendingPlaylists: boolean
|
||||
|
||||
artistPageParams: RefObject<string[]>
|
||||
artistPageParams: RefObject<Set<string>>
|
||||
albumPageParams: RefObject<string[]>
|
||||
|
||||
isFetchingNextTracksPage: boolean
|
||||
@@ -62,17 +64,12 @@ const LibraryContextInitializer = () => {
|
||||
|
||||
const { sortDescending, isFavorites } = useLibrarySortAndFilterContext()
|
||||
|
||||
const artistPageParams = useRef<string[]>([])
|
||||
const artistPageParams = useRef<Set<string>>(new Set<string>())
|
||||
|
||||
const albumPageParams = useRef<string[]>([])
|
||||
|
||||
const artistsInfiniteQuery = useInfiniteQuery({
|
||||
queryKey: [
|
||||
QueryKeys.AllArtistsAlphabetical,
|
||||
isFavorites,
|
||||
sortDescending,
|
||||
library?.musicLibraryId,
|
||||
],
|
||||
queryKey: [QueryKeys.InfiniteArtists, isFavorites, sortDescending, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchArtists(
|
||||
api,
|
||||
@@ -83,31 +80,50 @@ const LibraryContextInitializer = () => {
|
||||
[ItemSortBy.SortName],
|
||||
[sortDescending ? SortOrder.Descending : SortOrder.Ascending],
|
||||
),
|
||||
select: (data) => data.pages.flatMap((page) => [page.title, ...page.data]),
|
||||
initialPageParam: alphabet[0],
|
||||
maxPages: alphabet.length,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug(`Fetching next Artists page, last page: ${lastPage.title}`)
|
||||
console.debug(`fetching artist params: ${allPageParams.join(', ')}`)
|
||||
if (lastPageParam !== alphabet[alphabet.length - 1]) {
|
||||
artistPageParams.current = [
|
||||
...allPageParams,
|
||||
alphabet[alphabet.indexOf(lastPageParam) + 1],
|
||||
]
|
||||
return alphabet[alphabet.indexOf(lastPageParam) + 1]
|
||||
}
|
||||
select: (data) => {
|
||||
/**
|
||||
* A flattened array of all artists derived from the infinite query
|
||||
*/
|
||||
const flattenedArtistPages = data.pages.flatMap((page) => page)
|
||||
|
||||
return undefined
|
||||
/**
|
||||
* A set of letters we've seen so we can add them to the alphabetical selector
|
||||
*/
|
||||
const seenLetters = new Set<string>()
|
||||
|
||||
/**
|
||||
* The final array that will be provided to and rendered by the {@link Artists} component
|
||||
*/
|
||||
const flashArtistList: (string | number | BaseItemDto)[] = []
|
||||
|
||||
flattenedArtistPages.forEach((artist: BaseItemDto) => {
|
||||
const rawLetter = isString(artist.SortName)
|
||||
? artist.SortName.trim().charAt(0).toUpperCase()
|
||||
: '#'
|
||||
|
||||
/**
|
||||
* An alpha character or a hash if the artist's name doesn't start with a letter
|
||||
*/
|
||||
const letter = rawLetter.match(/[A-Z]/) ? rawLetter : '#'
|
||||
|
||||
if (!seenLetters.has(letter)) {
|
||||
seenLetters.add(letter)
|
||||
flashArtistList.push(letter)
|
||||
}
|
||||
|
||||
flashArtistList.push(artist)
|
||||
})
|
||||
|
||||
artistPageParams.current = seenLetters
|
||||
|
||||
return flashArtistList
|
||||
},
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined
|
||||
},
|
||||
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => {
|
||||
console.debug(`Artists first page: ${firstPage.title}`)
|
||||
artistPageParams.current = allPageParams
|
||||
if (firstPageParam !== alphabet[0]) {
|
||||
artistPageParams.current = allPageParams
|
||||
return alphabet[alphabet.indexOf(firstPageParam) - 1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
return firstPageParam === 0 ? null : firstPageParam - 1
|
||||
},
|
||||
})
|
||||
|
||||
@@ -377,7 +393,7 @@ const LibraryContext = createContext<LibraryContext>({
|
||||
hasNextAlbumsPage: false,
|
||||
isPendingTracks: false,
|
||||
isPendingAlbums: false,
|
||||
artistPageParams: { current: [] },
|
||||
artistPageParams: { current: new Set<string>() },
|
||||
albumPageParams: { current: [] },
|
||||
isFetchingNextTracksPage: false,
|
||||
isFetchingNextAlbumsPage: false,
|
||||
@@ -390,7 +406,7 @@ export const LibraryProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
() => context,
|
||||
[
|
||||
context.artistsInfiniteQuery.data,
|
||||
context.artistsInfiniteQuery.isPending,
|
||||
context.artistsInfiniteQuery.isFetching,
|
||||
context.tracks,
|
||||
context.albums,
|
||||
context.playlists,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Jellify is built with [`react-native-track-player`](https://rntp.dev) and has two context providers wrapped around it's functionality:
|
||||
|
||||
- `QueueContext`, exposed via `useQueueContext` hook
|
||||
- `QueueContext`, exposed via various hooks powered by
|
||||
- `PlayerProvider`, exposed via `usePlayerContext` hook
|
||||
|
||||
`react-native-track-player` manages it's own queue for sequential playback. Jellify manages it's own internal queue in state, relying on the `TrackPlayer`'s queue, and then exposes it to the rest of the app *synchronously*
|
||||
|
||||
@@ -25,7 +25,17 @@ import { trigger } from 'react-native-haptic-feedback'
|
||||
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useNetworkContext } from '../Network'
|
||||
import { useQueueContext } from './queue'
|
||||
import {
|
||||
usePlayQueueContext,
|
||||
useCurrentIndexContext,
|
||||
useQueueRefContext,
|
||||
useShuffledContext,
|
||||
useUnshuffledQueueContext,
|
||||
useSetUnshuffledQueueContext,
|
||||
useSetPlayQueueContext,
|
||||
useSetShuffledContext,
|
||||
useSetCurrentIndexContext,
|
||||
} from './queue'
|
||||
import { PlaystateApi } from '@jellyfin/sdk/lib/generated-client/api/playstate-api'
|
||||
import { networkStatusTypes } from '../../components/Network/internetConnectionWatcher'
|
||||
import { useJellifyContext } from '..'
|
||||
@@ -34,13 +44,9 @@ import { useSettingsContext } from '../Settings'
|
||||
import {
|
||||
getTracksToPreload,
|
||||
shouldStartPrefetching,
|
||||
optimizePlayerQueue,
|
||||
ensureUpcomingTracksInQueue,
|
||||
} from '../../player/helpers/gapless'
|
||||
import {
|
||||
PREFETCH_THRESHOLD_SECONDS,
|
||||
QUEUE_PREPARATION_THRESHOLD_SECONDS,
|
||||
} from '../../player/gapless-config'
|
||||
import { PREFETCH_THRESHOLD_SECONDS } from '../../player/gapless-config'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { shuffleJellifyTracks } from './utils/shuffle'
|
||||
|
||||
@@ -58,19 +64,16 @@ interface PlayerContext {
|
||||
|
||||
const PlayerContextInitializer = () => {
|
||||
const { api, sessionId } = useJellifyContext()
|
||||
const {
|
||||
playQueue,
|
||||
currentIndex,
|
||||
queueRef,
|
||||
skipping,
|
||||
setShuffled,
|
||||
setCurrentIndex,
|
||||
unshuffledQueue,
|
||||
setUnshuffledQueue,
|
||||
shuffled,
|
||||
setPlayQueue,
|
||||
} = useQueueContext()
|
||||
|
||||
const playQueue = usePlayQueueContext()
|
||||
const currentIndex = useCurrentIndexContext()
|
||||
const queueRef = useQueueRefContext()
|
||||
const unshuffledQueue = useUnshuffledQueueContext()
|
||||
const setUnshuffledQueue = useSetUnshuffledQueueContext()
|
||||
const setPlayQueue = useSetPlayQueueContext()
|
||||
const shuffled = useShuffledContext()
|
||||
const setShuffled = useSetShuffledContext()
|
||||
const setCurrentIndex = useSetCurrentIndexContext()
|
||||
const nowPlayingJson = storage.getString(MMKVStorageKeys.NowPlaying)
|
||||
const repeatModeJson = storage.getString(MMKVStorageKeys.RepeatMode)
|
||||
|
||||
@@ -565,7 +568,7 @@ const PlayerContextInitializer = () => {
|
||||
* Set the now playing track to the track at the current index in the play queue
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (currentIndex > -1 && playQueue.length > currentIndex && !skipping) {
|
||||
if (currentIndex > -1 && playQueue.length > currentIndex) {
|
||||
console.debug(`Setting now playing to queue index ${currentIndex}`)
|
||||
setNowPlaying(playQueue[currentIndex])
|
||||
}
|
||||
@@ -573,7 +576,7 @@ const PlayerContextInitializer = () => {
|
||||
if (currentIndex === -1) {
|
||||
setNowPlaying(undefined)
|
||||
}
|
||||
}, [currentIndex, playQueue, skipping])
|
||||
}, [currentIndex, playQueue])
|
||||
|
||||
/**
|
||||
* Initialize the player. This is used to load the queue from the {@link QueueProvider}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useContext, useEffect, useState, useMemo } from 'react'
|
||||
import { createContext } from 'react'
|
||||
import React, { ReactNode, useEffect, useState, useMemo, useCallback } from 'react'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
import { Section } from '../../components/Player/types'
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query'
|
||||
@@ -16,7 +15,7 @@ import TrackPlayer, { Event, Track, useTrackPlayerEvents } from 'react-native-tr
|
||||
import { findPlayQueueIndexStart } from './utils'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
|
||||
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import { filterTracksOnNetworkStatus } from './utils/queue'
|
||||
import { shuffleJellifyTracks } from './utils/shuffle'
|
||||
import { SKIP_TO_PREVIOUS_THRESHOLD } from '../../player/config'
|
||||
@@ -335,13 +334,13 @@ const QueueContextInitailizer = () => {
|
||||
|
||||
console.debug(`Final start index is ${finalStartIndex}`)
|
||||
|
||||
setPlayQueue(queue)
|
||||
setCurrentIndex(finalStartIndex)
|
||||
setQueueRef(queuingRef)
|
||||
|
||||
setPlayQueue(queue)
|
||||
await TrackPlayer.pause()
|
||||
await TrackPlayer.setQueue(queue)
|
||||
await TrackPlayer.skip(finalStartIndex)
|
||||
setCurrentIndex(finalStartIndex)
|
||||
|
||||
console.debug(
|
||||
`Queued ${queue.length} tracks, starting at ${finalStartIndex}${shuffleQueue ? ' (shuffled)' : ''}`,
|
||||
@@ -433,7 +432,7 @@ const QueueContextInitailizer = () => {
|
||||
console.debug(`Queue has ${newQueue.length} tracks`)
|
||||
}
|
||||
|
||||
const previous = async () => {
|
||||
const previous = useCallback(async () => {
|
||||
trigger('impactMedium')
|
||||
|
||||
const { position } = await TrackPlayer.getProgress()
|
||||
@@ -445,47 +444,50 @@ const QueueContextInitailizer = () => {
|
||||
if (currentIndex > 0 && Math.floor(position) < SKIP_TO_PREVIOUS_THRESHOLD) {
|
||||
TrackPlayer.skipToPrevious()
|
||||
} else await TrackPlayer.seekTo(0)
|
||||
}
|
||||
}, [currentIndex])
|
||||
|
||||
const skip = async (index: number | undefined = undefined) => {
|
||||
if (!isUndefined(index)) {
|
||||
const track = playQueue[index]
|
||||
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
|
||||
const queueIndex = queue.findIndex((t) => t.item.Id === track.item.Id)
|
||||
const skip = useCallback(
|
||||
async (index: number | undefined = undefined) => {
|
||||
if (!isUndefined(index)) {
|
||||
const track = playQueue[index]
|
||||
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
|
||||
const queueIndex = queue.findIndex((t) => t.item.Id === track.item.Id)
|
||||
|
||||
if (queueIndex !== -1) {
|
||||
// Track found in TrackPlayer queue, skip to it
|
||||
await TrackPlayer.skip(queueIndex)
|
||||
} else {
|
||||
// Track not found - ensure upcoming tracks are properly ordered
|
||||
console.debug('Track not found in TrackPlayer queue, updating upcoming tracks')
|
||||
try {
|
||||
await ensureUpcomingTracksInQueue(playQueue, currentIndex)
|
||||
if (queueIndex !== -1) {
|
||||
// Track found in TrackPlayer queue, skip to it
|
||||
await TrackPlayer.skip(queueIndex)
|
||||
} else {
|
||||
// Track not found - ensure upcoming tracks are properly ordered
|
||||
console.debug('Track not found in TrackPlayer queue, updating upcoming tracks')
|
||||
try {
|
||||
await ensureUpcomingTracksInQueue(playQueue, currentIndex)
|
||||
|
||||
// Now try to find the track again
|
||||
const updatedQueue = (await TrackPlayer.getQueue()) as JellifyTrack[]
|
||||
const updatedQueueIndex = updatedQueue.findIndex(
|
||||
(t) => t.item.Id === track.item.Id,
|
||||
)
|
||||
// Now try to find the track again
|
||||
const updatedQueue = (await TrackPlayer.getQueue()) as JellifyTrack[]
|
||||
const updatedQueueIndex = updatedQueue.findIndex(
|
||||
(t) => t.item.Id === track.item.Id,
|
||||
)
|
||||
|
||||
if (updatedQueueIndex !== -1) {
|
||||
await TrackPlayer.skip(updatedQueueIndex)
|
||||
} else {
|
||||
// If still not found, just update app state and let the system handle it
|
||||
await TrackPlayer.skip(index)
|
||||
console.debug('Updated app state to index', index)
|
||||
if (updatedQueueIndex !== -1) {
|
||||
await TrackPlayer.skip(updatedQueueIndex)
|
||||
} else {
|
||||
// If still not found, just update app state and let the system handle it
|
||||
await TrackPlayer.skip(index)
|
||||
console.debug('Updated app state to index', index)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to ensure upcoming tracks during skip:', error)
|
||||
// Fallback: just update app state
|
||||
setCurrentIndex(index)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to ensure upcoming tracks during skip:', error)
|
||||
// Fallback: just update app state
|
||||
setCurrentIndex(index)
|
||||
}
|
||||
} else {
|
||||
// Default next track behavior
|
||||
await TrackPlayer.skipToNext()
|
||||
}
|
||||
} else {
|
||||
// Default next track behavior
|
||||
await TrackPlayer.skipToNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentIndex, playQueue],
|
||||
)
|
||||
//#endregion Functions
|
||||
|
||||
//#region Hooks
|
||||
@@ -744,28 +746,31 @@ const QueueContextInitailizer = () => {
|
||||
//#endregion useEffect(s)
|
||||
|
||||
//#region Return
|
||||
return {
|
||||
queueRef,
|
||||
playQueue,
|
||||
setPlayQueue,
|
||||
currentIndex,
|
||||
setCurrentIndex,
|
||||
skipping,
|
||||
fetchQueueSectionData,
|
||||
loadQueue,
|
||||
useAddToQueue,
|
||||
useLoadNewQueue,
|
||||
useRemoveFromQueue,
|
||||
useRemoveUpcomingTracks,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
shuffled,
|
||||
setShuffled,
|
||||
unshuffledQueue,
|
||||
setUnshuffledQueue,
|
||||
resetQueue,
|
||||
}
|
||||
return useMemo(
|
||||
() => ({
|
||||
queueRef,
|
||||
playQueue,
|
||||
setPlayQueue,
|
||||
currentIndex,
|
||||
setCurrentIndex,
|
||||
skipping,
|
||||
fetchQueueSectionData,
|
||||
loadQueue,
|
||||
useAddToQueue,
|
||||
useLoadNewQueue,
|
||||
useRemoveFromQueue,
|
||||
useRemoveUpcomingTracks,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
shuffled,
|
||||
setShuffled,
|
||||
unshuffledQueue,
|
||||
setUnshuffledQueue,
|
||||
resetQueue,
|
||||
}),
|
||||
[currentIndex, playQueue, shuffled, skipping],
|
||||
)
|
||||
//#endregion Return
|
||||
}
|
||||
|
||||
@@ -849,17 +854,53 @@ export const QueueProvider: ({ children }: { children: ReactNode }) => React.JSX
|
||||
children: ReactNode
|
||||
}) => {
|
||||
// Add performance monitoring
|
||||
const performanceMetrics = usePerformanceMonitor('QueueProvider', 5)
|
||||
usePerformanceMonitor('QueueProvider', 5)
|
||||
|
||||
const context = QueueContextInitailizer()
|
||||
|
||||
// Memoize the context value to prevent unnecessary re-renders
|
||||
const value = useMemo(
|
||||
() => context,
|
||||
[context.currentIndex, context.shuffled, context.skipping, context.playQueue],
|
||||
)
|
||||
|
||||
return <QueueContext.Provider value={value}>{children}</QueueContext.Provider>
|
||||
return <QueueContext.Provider value={context}>{children}</QueueContext.Provider>
|
||||
}
|
||||
|
||||
export const useQueueContext = () => useContext(QueueContext)
|
||||
|
||||
export const useCurrentIndexContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.currentIndex)
|
||||
export const useQueueRefContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.queueRef)
|
||||
export const usePlayQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.playQueue)
|
||||
export const useUnshuffledQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.unshuffledQueue)
|
||||
export const useShuffledContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.shuffled)
|
||||
|
||||
export const useSkippingContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.skipping)
|
||||
|
||||
export const useFetchQueueSectionDataContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.fetchQueueSectionData)
|
||||
|
||||
export const useLoadQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.useLoadNewQueue)
|
||||
export const useAddToQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.useAddToQueue)
|
||||
export const useRemoveFromQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.useRemoveFromQueue)
|
||||
export const useRemoveUpcomingTracksContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.useRemoveUpcomingTracks)
|
||||
export const useReorderQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.useReorderQueue)
|
||||
export const useSkipContext = () => useContextSelector(QueueContext, (context) => context.useSkip)
|
||||
export const usePreviousContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.usePrevious)
|
||||
|
||||
export const useResetQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.resetQueue)
|
||||
export const useSetShuffledContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.setShuffled)
|
||||
export const useSetUnshuffledQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.setUnshuffledQueue)
|
||||
export const useSetPlayQueueContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.setPlayQueue)
|
||||
export const useSetCurrentIndexContext = () =>
|
||||
useContextSelector(QueueContext, (context) => context.setCurrentIndex)
|
||||
|
||||
@@ -6,12 +6,12 @@ import Button from '../../components/Global/helpers/button'
|
||||
import Icon from '../../components/Global/components/icon'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { useNetworkContext } from '../../providers/Network'
|
||||
import { useQueueContext } from '../../providers/Player/queue'
|
||||
import { useResetQueueContext } from '../../providers/Player/queue'
|
||||
|
||||
export default function SignOutModal({ navigation }: SignOutModalProps): React.JSX.Element {
|
||||
const { server } = useJellifyContext()
|
||||
|
||||
const { resetQueue } = useQueueContext()
|
||||
const resetQueue = useResetQueueContext()
|
||||
const { clearDownloads } = useNetworkContext()
|
||||
|
||||
return (
|
||||
|
||||
61
yarn.lock
61
yarn.lock
@@ -2106,18 +2106,18 @@
|
||||
invariant "^2.2.4"
|
||||
nullthrows "^1.1.1"
|
||||
|
||||
"@react-navigation/bottom-tabs@^7.4.4":
|
||||
version "7.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.4.tgz#8896a8877b679da9aeb2ed7dda3b4964a88e4863"
|
||||
integrity sha512-/YEBu/cZUgYAaNoSfUnqoRjpbt8NOsb5YvDiKVyTcOOAF1GTbUw6kRi+AGW1Sm16CqzabO/TF2RvN1RmPS9VHg==
|
||||
"@react-navigation/bottom-tabs@^7.4.5":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.5.tgz#d506184e091cd00be23a4eee31014f816d58c635"
|
||||
integrity sha512-Kt2/E9O7G4Kvx/NYrMVCSk+RN5auZcLgfBeCf5/vH6/weCJOxBy15Wav+3HiehZLNcQ40FJ3PeNHF0CR04idSw==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^2.6.1"
|
||||
"@react-navigation/elements" "^2.6.2"
|
||||
color "^4.2.3"
|
||||
|
||||
"@react-navigation/core@^7.12.3":
|
||||
version "7.12.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.12.3.tgz#acb2edbda8e34cf73005dcc463c2e6013cdec43d"
|
||||
integrity sha512-oEz5sL8KTYmCv8SQX1A4k75A7VzYadOCudp/ewOBqRXOmZdxDQA9JuN7baE9IVyaRW0QTVDy+N/Wnqx9F4aW6A==
|
||||
"@react-navigation/core@^7.12.4":
|
||||
version "7.12.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.12.4.tgz#73cc4c0989455c93bf21d7aeecc89d3a7006ccde"
|
||||
integrity sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q==
|
||||
dependencies:
|
||||
"@react-navigation/routers" "^7.5.1"
|
||||
escape-string-regexp "^4.0.0"
|
||||
@@ -2127,38 +2127,38 @@
|
||||
use-latest-callback "^0.2.4"
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
||||
"@react-navigation/elements@^2.6.1":
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.6.1.tgz#c101f0b72b48024c8fe3ee09eccb871fa398f761"
|
||||
integrity sha512-kVbIo+5FaqJv6MiYUR6nQHiw+10dmmH/P10C29wrH9S6fr7k69fImHGeiOI/h7SMDJ2vjWhftyEjqYO+c2LG/w==
|
||||
"@react-navigation/elements@^2.6.2":
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.6.2.tgz#736c06d0a5f0100932293a0a7f0230a44b6a335b"
|
||||
integrity sha512-F3xySYWSmzIKSRUlisRSvqE6wzafPJwlgP8hZmf5cnctIu+gJUYWX4e2Za4dQZCELg7luFQjj1ohPWPqm/ziUQ==
|
||||
dependencies:
|
||||
color "^4.2.3"
|
||||
use-latest-callback "^0.2.4"
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
||||
"@react-navigation/material-top-tabs@^7.3.4":
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/material-top-tabs/-/material-top-tabs-7.3.4.tgz#9509eb503920e3fb55626a2a028e3a9b9d5153a0"
|
||||
integrity sha512-sHBiIszq6FumBu8TboN+nVyWxgwbAER6UYULllbN87dDgnUtf+BucUYRAa+2pWeZBA2Q1esYl6VFj6pEFk2how==
|
||||
"@react-navigation/material-top-tabs@^7.3.5":
|
||||
version "7.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/material-top-tabs/-/material-top-tabs-7.3.5.tgz#d9f8ae7b44d6181d204aecad0948392664e7c491"
|
||||
integrity sha512-qIsKCWUnmrojoeH2gnG1xG2AEWUu/SAaPwqfUPEwLwUlPoCbCKDR7ZIFd0Y5yPkwVVSrW3A8VGLlj/gjMDdHsQ==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^2.6.1"
|
||||
"@react-navigation/elements" "^2.6.2"
|
||||
color "^4.2.3"
|
||||
react-native-tab-view "^4.1.2"
|
||||
|
||||
"@react-navigation/native-stack@^7.3.23":
|
||||
version "7.3.23"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.3.23.tgz#29d9ba22d894ce2088ad672c7125dd1aebf96a0a"
|
||||
integrity sha512-WQBBnPrlM0vXj5YAFnJTyrkiCyANl2KnBV8ZmUG61HkqXFwuBbnHij6eoggXH1VZkEVRxW8k0E3qqfPtEZfUjQ==
|
||||
"@react-navigation/native-stack@^7.3.24":
|
||||
version "7.3.24"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.3.24.tgz#16f777a059272427fb35dea9a83619565ff27250"
|
||||
integrity sha512-Q2kWil9K5GxU2wKkmAgbkEdctiy0NpFafaIbRM0YzpMNbqzrrPwDaNjOO4A3BEVY6YO4ldg+TpJdj37YyD/qPQ==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^2.6.1"
|
||||
"@react-navigation/elements" "^2.6.2"
|
||||
warn-once "^0.1.1"
|
||||
|
||||
"@react-navigation/native@^7.1.16":
|
||||
version "7.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.16.tgz#2474b717d77da8857c5c55c608847aac6391d2e2"
|
||||
integrity sha512-JnnK81JYJ6PiMsuBEshPGHwfagRnH8W7SYdWNrPxQdNtakkHtG4u0O9FmrOnKiPl45DaftCcH1g+OVTFFgWa0Q==
|
||||
"@react-navigation/native@^7.1.17":
|
||||
version "7.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.17.tgz#88d557c0f5000aa2741e4368c59719526f1394c4"
|
||||
integrity sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ==
|
||||
dependencies:
|
||||
"@react-navigation/core" "^7.12.3"
|
||||
"@react-navigation/core" "^7.12.4"
|
||||
escape-string-regexp "^4.0.0"
|
||||
fast-deep-equal "^3.1.3"
|
||||
nanoid "^3.3.11"
|
||||
@@ -9767,6 +9767,11 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-context-selector@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-2.0.0.tgz#3b5dafec7aa947c152d4f0aa7f250e99a205df3d"
|
||||
integrity sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==
|
||||
|
||||
use-latest-callback@^0.2.4:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.4.tgz#35c0f028f85a3f4cf025b06011110e87cc18f57e"
|
||||
|
||||
Reference in New Issue
Block a user