Add A-Z Selector for Tracks, Fix issue where home screen didn't load fully on sign in (#505)

Remove Home and Library Context Providers, move their data fetching functionality to reusable Tanstack Query hooks

Fix an issue playlists not showing up after creating

fix issue where the home screen didn't fully load on sign in
This commit is contained in:
Violet Caulfield
2025-09-04 16:35:59 -05:00
committed by GitHub
parent 7b8a4876bf
commit 4905ccfd2a
60 changed files with 877 additions and 993 deletions

View File

@@ -6,10 +6,9 @@ import App from './App'
import { name as appName } from './app.json'
import { PlaybackService } from './src/player/service'
import TrackPlayer from 'react-native-track-player'
import { enableFreeze, enableScreens } from 'react-native-screens'
import { enableScreens } from 'react-native-screens'
enableScreens(true)
enableFreeze(true)
AppRegistry.registerComponent(appName, () => App)
AppRegistry.registerComponent('RNCarPlayScene', () => App)

View File

@@ -2757,7 +2757,7 @@ PODS:
- RNWorklets
- SocketRocket
- Yoga
- RNScreens (4.15.4):
- RNScreens (4.16.0):
- boost
- DoubleConversion
- fast_float
@@ -2784,10 +2784,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNScreens/common (= 4.15.4)
- RNScreens/common (= 4.16.0)
- SocketRocket
- Yoga
- RNScreens/common (4.15.4):
- RNScreens/common (4.16.0):
- boost
- DoubleConversion
- fast_float
@@ -3354,7 +3354,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
RNReanimated: ee96d03fe3713993a30cc205522792b4cb08e4f9
RNScreens: db22525a8ed56bb87ab038b8f03a050bf40e6ed8
RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c
RNSentry: 95e1ed0ede28a4af58aaafedeac9fcfaba0e89ce
RNWorklets: e8335dff9d27004709f58316985769040cd1e8f2
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d

View File

@@ -61,7 +61,6 @@
"lodash": "^4.17.21",
"openai": "^5.16.0",
"react": "19.1.0",
"react-freeze": "^1.0.4",
"react-native": "0.81.1",
"react-native-background-actions": "^4.0.1",
"react-native-blob-util": "^0.22.2",
@@ -83,7 +82,7 @@
"react-native-pager-view": "^7.0.0",
"react-native-reanimated": "4.0.2",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "4.15.4",
"react-native-screens": "4.16.0",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.15.0",
"react-native-toast-message": "^2.3.3",
@@ -94,7 +93,6 @@
"ruby": "^0.6.1",
"scheduler": "^0.26.0",
"tamagui": "^1.132.23",
"use-context-selector": "^2.0.0",
"zustand": "^5.0.8"
},
"devDependencies": {

View File

@@ -9,7 +9,7 @@ import { RefObject, useCallback, useRef } from 'react'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
import { ApiLimits } from '../query.config'
import { fetchRecentlyAdded } from '../recents'
import { fetchRecentlyAdded } from '../recents/utils'
import { queryClient } from '../../../constants/query-client'
const useAlbums: () => [

View File

@@ -0,0 +1,40 @@
import { useInfiniteQuery } from '@tanstack/react-query'
import { FrequentlyPlayedArtistsQueryKey, FrequentlyPlayedTracksQueryKey } from './keys'
import { useJellifyContext } from '../../../providers'
import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from './utils/frequents'
import { ApiLimits } from '../query.config'
import { queryClient } from '../../../constants/query-client'
import { isUndefined } from 'lodash'
export const useFrequentlyPlayedTracks = () => {
const { api, user, library } = useJellifyContext()
return useInfiniteQuery({
queryKey: FrequentlyPlayedTracksQueryKey(user, library),
queryFn: ({ pageParam }) => fetchFrequentlyPlayed(api, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for frequently played')
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
},
})
}
export const useFrequentlyPlayedArtists = () => {
const { api, user, library } = useJellifyContext()
const { data: frequentlyPlayedTracks } = useFrequentlyPlayedTracks()
return useInfiniteQuery({
queryKey: FrequentlyPlayedArtistsQueryKey(user, library),
queryFn: ({ pageParam }) => fetchFrequentlyPlayedArtists(api, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for frequent artists')
return lastPage.length > 0 ? lastPageParam + 1 : undefined
},
enabled: !isUndefined(frequentlyPlayedTracks),
})
}

View File

@@ -0,0 +1,23 @@
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
import { JellifyUser } from '@/src/types/JellifyUser'
enum FrequentsQueryKeys {
FrequentlyPlayedTracks = 'FREQUENTLY_PLAYED_TRACKS',
FrequentlyPlayedArtists = 'FREQUENTLY_PLAYED_ARTISTS',
}
const FrequentsQueryKey = (
key: FrequentsQueryKeys,
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
) => [key, user?.id, library?.musicLibraryId]
export const FrequentlyPlayedTracksQueryKey = (
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
) => FrequentsQueryKey(FrequentsQueryKeys.FrequentlyPlayedTracks, user, library)
export const FrequentlyPlayedArtistsQueryKey = (
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
) => FrequentsQueryKey(FrequentsQueryKeys.FrequentlyPlayedArtists, user, library)

View File

@@ -7,8 +7,9 @@ import {
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { Api } from '@jellyfin/sdk'
import { isUndefined } from 'lodash'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import { fetchItem } from './item'
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
import { fetchItem } from '../../item'
import { ApiLimits } from '../../query.config'
/**
* Fetches the 100 most frequently played items from the user's library
@@ -31,8 +32,8 @@ export function fetchFrequentlyPlayed(
includeItemTypes: [BaseItemKind.Audio],
parentId: library!.musicLibraryId,
recursive: true,
limit: 100,
startIndex: page * 100,
limit: ApiLimits.Home,
startIndex: page * ApiLimits.Home,
sortBy: [ItemSortBy.PlayCount],
sortOrder: [SortOrder.Descending],
enableUserData: true,

View File

@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query'
import { useFrequentlyPlayedArtists, useFrequentlyPlayedTracks } from '../frequents'
import { useRecentArtists, useRecentlyPlayedTracks } from '../recents'
const useHomeQueries = () => {
const { refetch: refetchRecentArtists } = useRecentArtists()
const { refetch: refetchRecentlyPlayed } = useRecentlyPlayedTracks()
const { refetch: refetchFrequentArtists } = useFrequentlyPlayedArtists()
const { refetch: refetchFrequentlyPlayed } = useFrequentlyPlayedTracks()
return useQuery({
queryKey: ['Home'],
queryFn: async () => {
await Promise.all([refetchRecentlyPlayed, refetchFrequentlyPlayed])
await Promise.all([refetchFrequentArtists, refetchRecentArtists])
return true
},
})
}
export default useHomeQueries

View File

@@ -11,7 +11,6 @@ import { SectionList } from 'react-native'
import { Api } from '@jellyfin/sdk/lib/api'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import QueryConfig from './query.config'
import { alphabet } from '../../providers/Library'
import { JellifyUser } from '../../types/JellifyUser'
/**
@@ -57,7 +56,7 @@ export async function fetchItems(
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
types: BaseItemKind[],
page: string | number = 0,
page: number = 0,
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
sortOrder: SortOrder[] = [SortOrder.Ascending],
isFavorite?: boolean | undefined,
@@ -81,8 +80,6 @@ export async function fetchItems(
fields: [ItemFields.ChildCount, ItemFields.SortName],
startIndex: typeof page === 'number' ? page * QueryConfig.limits.library : 0,
limit: QueryConfig.limits.library,
nameStartsWith: typeof page === 'string' && page !== alphabet[0] ? page : undefined,
nameLessThan: typeof page === 'string' && page === alphabet[0] ? 'A' : undefined,
isFavorite,
ids,
})

View File

@@ -2,26 +2,12 @@ import { Api } from '@jellyfin/sdk'
import { useJellifyContext } from '../../../../src/providers'
import { useQuery } from '@tanstack/react-query'
import { JellifyUser } from '@/src/types/JellifyUser'
import { DeviceProfile } from '@jellyfin/sdk/lib/generated-client'
import useStreamingDeviceProfile, {
useDownloadingDeviceProfile,
} from '../../../stores/device-profile'
import { fetchMediaInfo } from './utils'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
interface MediaInfoQueryProps {
api: Api | undefined
user: JellifyUser | undefined
deviceProfile: DeviceProfile | undefined
itemId: string | null | undefined
}
const mediaInfoQueryKey = ({ api, user, deviceProfile, itemId }: MediaInfoQueryProps) => [
api,
user,
deviceProfile?.Name,
itemId,
]
import MediaInfoQueryKey from './keys'
/**
* A React hook that will retrieve the latest media info
@@ -38,13 +24,13 @@ const mediaInfoQueryKey = ({ api, user, deviceProfile, itemId }: MediaInfoQueryP
* @returns
*/
const useStreamedMediaInfo = (itemId: string | null | undefined) => {
const { api, user } = useJellifyContext()
const { api } = useJellifyContext()
const deviceProfile = useStreamingDeviceProfile()
return useQuery({
queryKey: mediaInfoQueryKey({ api, user, deviceProfile, itemId }),
queryFn: () => fetchMediaInfo(api, user, deviceProfile, itemId),
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
staleTime: Infinity, // Only refetch when the user's device profile changes
})
}
@@ -66,13 +52,13 @@ export default useStreamedMediaInfo
* @returns
*/
export const useDownloadedMediaInfo = (itemId: string | null | undefined) => {
const { api, user } = useJellifyContext()
const { api } = useJellifyContext()
const deviceProfile = useDownloadingDeviceProfile()
return useQuery({
queryKey: mediaInfoQueryKey({ api, user, deviceProfile, itemId }),
queryFn: () => fetchMediaInfo(api, user, deviceProfile, itemId),
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
staleTime: Infinity, // Only refetch when the user's device profile changes
})
}

View File

@@ -0,0 +1,16 @@
import { Api } from '@jellyfin/sdk'
import { DeviceProfile } from '@jellyfin/sdk/lib/generated-client'
interface MediaInfoQueryProps {
api: Api | undefined
deviceProfile: DeviceProfile | undefined
itemId: string | null | undefined
}
const MediaInfoQueryKey = ({ api, deviceProfile, itemId }: MediaInfoQueryProps) => [
'MEDIA_INFO',
api,
deviceProfile?.Name,
itemId,
]
export default MediaInfoQueryKey

View File

@@ -2,11 +2,9 @@ import { Api } from '@jellyfin/sdk'
import { DeviceProfile, PlaybackInfoResponse } from '@jellyfin/sdk/lib/generated-client/models'
import { getMediaInfoApi } from '@jellyfin/sdk/lib/utils/api'
import { isUndefined } from 'lodash'
import { JellifyUser } from '../../../../types/JellifyUser'
export async function fetchMediaInfo(
api: Api | undefined,
user: JellifyUser | undefined,
deviceProfile: DeviceProfile | undefined,
itemId: string | null | undefined,
): Promise<PlaybackInfoResponse> {
@@ -14,12 +12,10 @@ export async function fetchMediaInfo(
return new Promise((resolve, reject) => {
if (isUndefined(api)) return reject('Client instance not set')
if (isUndefined(user)) return reject('User instance not set')
getMediaInfoApi(api)
.getPostedPlaybackInfo({
itemId: itemId!,
userId: user.id,
playbackInfoDto: {
DeviceProfile: deviceProfile,
},

View File

@@ -0,0 +1,19 @@
import { useJellifyContext } from '../../../providers'
import { UserPlaylistsQueryKey } from './keys'
import { useInfiniteQuery } from '@tanstack/react-query'
import { fetchUserPlaylists, fetchPublicPlaylists } from './utils'
import { ApiLimits } from '../query.config'
export const useUserPlaylists = () => {
const { api, user, library } = useJellifyContext()
return useInfiniteQuery({
queryKey: UserPlaylistsQueryKey(library),
queryFn: () => fetchUserPlaylists(api, user, library),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
},
})
}

View File

@@ -0,0 +1,16 @@
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
enum PlaylistQueryKeys {
UserPlaylists,
PublicPlaylists,
}
export const UserPlaylistsQueryKey = (library: JellifyLibrary | undefined) => [
PlaylistQueryKeys.UserPlaylists,
library?.playlistLibraryId,
]
export const PublicPlaylistsQueryKey = (library: JellifyLibrary | undefined) => [
PlaylistQueryKeys.PublicPlaylists,
library?.playlistLibraryId,
]

View File

@@ -1,10 +1,15 @@
import { BaseItemDto, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
import {
BaseItemDto,
ItemFields,
ItemSortBy,
SortOrder,
} from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { JellifyUser } from '../../types/JellifyUser'
import { JellifyUser } from '../../../../types/JellifyUser'
import { Api } from '@jellyfin/sdk'
import { isUndefined } from 'lodash'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import QueryConfig from './query.config'
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
import QueryConfig from '../../query.config'
export async function fetchUserPlaylists(
api: Api | undefined,
@@ -27,7 +32,12 @@ export async function fetchUserPlaylists(
.getItems({
userId: user.id,
parentId: library.playlistLibraryId!,
fields: ['Path', 'CanDelete', 'Genres'],
fields: [
ItemFields.Path,
ItemFields.CanDelete,
ItemFields.Genres,
ItemFields.ChildCount,
],
sortBy: [ItemSortBy.SortName],
sortOrder: [SortOrder.Ascending],
limit: QueryConfig.limits.library,

View File

@@ -1,7 +1,9 @@
import { ImageFormat } from '@jellyfin/sdk/lib/generated-client/models'
export enum ApiLimits {
Library = 200,
Home = 100,
Library = 400,
Discover = 50,
}
const QueryConfig = {

View File

@@ -0,0 +1,40 @@
import { useJellifyContext } from '../../../providers'
import { RecentlyPlayedArtistsQueryKey, RecentlyPlayedTracksQueryKey } from './keys'
import { useInfiniteQuery } from '@tanstack/react-query'
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from './utils'
import { ApiLimits } from '../query.config'
import { queryClient } from '../../../constants/query-client'
import { isUndefined } from 'lodash'
export const useRecentlyPlayedTracks = () => {
const { api, user, library } = useJellifyContext()
return useInfiniteQuery({
queryKey: RecentlyPlayedTracksQueryKey(user, library),
queryFn: ({ pageParam }) => fetchRecentlyPlayed(api, user, library, pageParam),
initialPageParam: 0,
select: (data) => data.pages.flatMap((page) => page),
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for recent tracks')
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
},
})
}
export const useRecentArtists = () => {
const { api, user, library } = useJellifyContext()
const { data: recentlyPlayedTracks } = useRecentlyPlayedTracks()
return useInfiniteQuery({
queryKey: RecentlyPlayedArtistsQueryKey(user, library),
queryFn: ({ pageParam }) => fetchRecentlyPlayedArtists(api, user, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for recent artists')
return lastPage.length > 0 ? lastPageParam + 1 : undefined
},
enabled: !isUndefined(recentlyPlayedTracks),
})
}

View File

@@ -0,0 +1,24 @@
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
import { JellifyUser } from '@/src/types/JellifyUser'
enum RecentsQueryKeys {
RecentlyPlayedTracks = 'RECENTLY_PLAYED_TRACKS',
RecentlyPlayedArtists = 'RECENTLY_PLAYED_ARTISTS',
RecentlyAdded = 'RECENTLY_ADDED',
}
const RecentsQueryKey = (
key: RecentsQueryKeys,
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
) => [key, user?.id, library?.musicLibraryId]
export const RecentlyPlayedTracksQueryKey = (
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
) => RecentsQueryKey(RecentsQueryKeys.RecentlyPlayedTracks, user, library)
export const RecentlyPlayedArtistsQueryKey = (
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
) => RecentsQueryKey(RecentsQueryKeys.RecentlyPlayedArtists, user, library)

View File

@@ -6,16 +6,17 @@ import {
SortOrder,
} from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
import QueryConfig from './query.config'
import QueryConfig, { ApiLimits } from '../../query.config'
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
import { Api } from '@jellyfin/sdk'
import { isUndefined } from 'lodash'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import { JellifyUser } from '../../types/JellifyUser'
import { queryClient } from '../../constants/query-client'
import { QueryKeys } from '../../enums/query-keys'
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
import { JellifyUser } from '../../../../types/JellifyUser'
import { queryClient } from '../../../../constants/query-client'
import { QueryKeys } from '../../../../enums/query-keys'
import { InfiniteData } from '@tanstack/react-query'
import { fetchItems } from './item'
import { fetchItems } from '../../item'
import { RecentlyPlayedTracksQueryKey } from '../keys'
export async function fetchRecentlyAdded(
api: Api | undefined,
@@ -29,7 +30,7 @@ export async function fetchRecentlyAdded(
getUserLibraryApi(api)
.getLatestMedia({
parentId: library.musicLibraryId,
limit: QueryConfig.limits.recents,
limit: ApiLimits.Discover,
})
.then(({ data }) => {
if (data) return resolve(data)
@@ -53,7 +54,7 @@ export async function fetchRecentlyPlayed(
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
page: number,
limit: number = QueryConfig.limits.recents,
limit: number = ApiLimits.Home,
): Promise<BaseItemDto[]> {
console.debug('Fetching recently played items')
@@ -105,10 +106,9 @@ export function fetchRecentlyPlayedArtists(
if (isUndefined(library)) return reject('Library instance not set')
// Get the recently played tracks from the query client
const recentlyPlayedTracks = queryClient.getQueryData<InfiniteData<BaseItemDto[]>>([
QueryKeys.RecentlyPlayed,
library.musicLibraryId,
])
const recentlyPlayedTracks = queryClient.getQueryData<InfiniteData<BaseItemDto[]>>(
RecentlyPlayedTracksQueryKey(user, library),
)
if (!recentlyPlayedTracks) {
return resolve([])
}

View File

@@ -0,0 +1,90 @@
import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'
import { TracksQueryKey } from './keys'
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
import { useJellifyContext } from '../../../providers'
import fetchTracks from './utils'
import {
BaseItemDto,
ItemSortBy,
SortOrder,
UserItemDataDto,
} from '@jellyfin/sdk/lib/generated-client'
import { RefObject, useCallback, useRef } from 'react'
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
import { ApiLimits } from '../query.config'
import { useAllDownloadedTracks } from '../download'
import { queryClient } from '../../../constants/query-client'
import UserDataQueryKey from '../user-data/keys'
import { JellifyUser } from '@/src/types/JellifyUser'
const useTracks: () => [
RefObject<Set<string>>,
UseInfiniteQueryResult<(string | number | BaseItemDto)[]>,
] = () => {
const { api, user, library } = useJellifyContext()
const { isFavorites, sortDescending, isDownloaded } = useLibrarySortAndFilterContext()
const { data: downloadedTracks } = useAllDownloadedTracks()
const trackPageParams = useRef<Set<string>>(new Set<string>())
const selectTracks = useCallback(
(data: InfiniteData<BaseItemDto[], unknown>) =>
flattenInfiniteQueryPages(data, trackPageParams),
[],
)
const tracksInfiniteQuery = useInfiniteQuery({
queryKey: TracksQueryKey(
isFavorites ?? false,
isDownloaded,
sortDescending,
library,
downloadedTracks?.length,
),
queryFn: ({ pageParam }) => {
if (!isDownloaded)
return fetchTracks(
api,
user,
library,
pageParam,
isFavorites,
ItemSortBy.Name,
sortDescending ? SortOrder.Descending : SortOrder.Ascending,
)
else
return (downloadedTracks ?? [])
.map(({ item }) => item)
.sort((a, b) => {
if ((a.Name ?? '') < (b.Name ?? '')) return -1
else if ((a.Name ?? '') === (b.Name ?? '')) return 0
else return 1
})
.filter((track) => {
if (!isFavorites) return true
else return isDownloadedTrackAlsoFavorite(user, track)
})
},
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
if (isDownloaded) return undefined
else return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
},
select: selectTracks,
})
return [trackPageParams, tracksInfiniteQuery]
}
export default useTracks
function isDownloadedTrackAlsoFavorite(user: JellifyUser | undefined, track: BaseItemDto): boolean {
if (!user) return false
const userData = queryClient.getQueryData(UserDataQueryKey(user!, track)) as
| UserItemDataDto
| undefined
return userData?.IsFavorite ?? false
}

View File

@@ -0,0 +1,20 @@
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
enum TrackQueryKeys {
AllTracks = 'ALL_TRACKS',
PlaylistTracks = 'PLAYLIST_TRACKS',
}
export const TracksQueryKey = (
isFavorites: boolean,
isDownloaded: boolean,
sortDescending: boolean,
library: JellifyLibrary | undefined,
downloads: number | undefined,
) => [
TrackQueryKeys.AllTracks,
library?.musicLibraryId,
isFavorites,
isDownloaded ? `${isDownloaded} - ${downloads}` : isDownloaded,
sortDescending,
]

View File

@@ -1,17 +1,18 @@
import { JellifyLibrary } from '../../types/JellifyLibrary'
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
import { Api } from '@jellyfin/sdk'
import {
BaseItemDto,
BaseItemKind,
ItemFields,
ItemSortBy,
SortOrder,
} from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { isUndefined } from 'lodash'
import QueryConfig from './query.config'
import { JellifyUser } from '../../types/JellifyUser'
import { ApiLimits } from '../../query.config'
import { JellifyUser } from '../../../../types/JellifyUser'
export function fetchTracks(
export default function fetchTracks(
api: Api | undefined,
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
@@ -20,7 +21,7 @@ export function fetchTracks(
sortBy: ItemSortBy = ItemSortBy.SortName,
sortOrder: SortOrder = SortOrder.Ascending,
) {
console.debug('Fetching tracks', isFavorite)
console.debug('Fetching tracks', pageParam)
return new Promise<BaseItemDto[]>((resolve, reject) => {
if (isUndefined(api)) return reject('Client instance not set')
if (isUndefined(library)) return reject('Library instance not set')
@@ -34,14 +35,13 @@ export function fetchTracks(
userId: user.id,
recursive: true,
isFavorite: isFavorite,
limit: QueryConfig.limits.library,
startIndex: pageParam * QueryConfig.limits.library,
limit: ApiLimits.Library,
startIndex: pageParam * ApiLimits.Library,
sortBy: [sortBy],
sortOrder: [sortOrder],
fields: [ItemFields.SortName],
})
.then((response) => {
console.debug(`Received favorite artist response`, response)
if (response.data.Items) return resolve(response.data.Items)
else return resolve([])
})

View File

@@ -3,7 +3,6 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item
import { useQuery } from '@tanstack/react-query'
import fetchUserData from './utils'
import UserDataQueryKey from './keys'
import { ONE_MINUTE } from '@/src/constants/query-client'
export const useIsFavorite = (item: BaseItemDto) => {
const { api, user } = useJellifyContext()

View File

@@ -2,7 +2,6 @@ import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
import { useJellifyContext } from '../../providers'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { QueryKeys } from '../../enums/query-keys'
import { fetchUserPlaylists } from '../../api/queries/playlists'
import { addToPlaylist } from '../../api/mutations/playlists'
import QueryConfig from '../../api/queries/query.config'
import { queryClient } from '../../constants/query-client'
@@ -28,27 +27,19 @@ 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'
export default function AddToPlaylist({ track }: { track: BaseItemDto }): React.JSX.Element {
const { api, user, library } = useJellifyContext()
const { api, user } = useJellifyContext()
const trigger = useHapticFeedback()
const {
data: playlists,
refetch,
isPending: playlistsFetchPending,
isSuccess: playlistsFetchSuccess,
} = useInfiniteQuery({
queryKey: [QueryKeys.Playlists],
queryFn: () => fetchUserPlaylists(api, user, library),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
return lastPage.length === QueryConfig.limits.library * 2
? lastPageParam + 1
: undefined
},
})
} = useUserPlaylists()
// Fetch all playlist tracks to check if the current track is already in any playlists
const playlistsWithTracks = useQuery({
@@ -96,9 +87,7 @@ export default function AddToPlaylist({ track }: { track: BaseItemDto }): React.
trigger('notificationSuccess')
queryClient.invalidateQueries({
queryKey: [QueryKeys.Playlists],
})
refetch
queryClient.invalidateQueries({
queryKey: [QueryKeys.ItemTracks, playlist.Id!],

View File

@@ -25,7 +25,6 @@ import LibraryStackParamList from '../../screens/Library/types'
import DiscoverStackParamList from '../../screens/Discover/types'
import { BaseStackParamList } from '../../screens/types'
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../../stores/device-profile'
import { useAllDownloadedTracks } from '../../api/queries/download'
/**
* The screen for an Album's track list
@@ -47,8 +46,6 @@ export function Album(): React.JSX.Element {
const downloadingDeviceProfile = useDownloadingDeviceProfile()
const { mutate: loadNewQueue } = useLoadNewQueue()
const { data: downloadedTracks } = useAllDownloadedTracks()
const downloadAlbum = (item: BaseItemDto[]) => {
if (!api) return
const jellifyTracks = item.map((item) =>

View File

@@ -83,9 +83,6 @@ export default function Albums({
<XStack flex={1}>
<FlashList
ref={sectionListRef}
contentContainerStyle={{
paddingTop: getToken('$1'),
}}
contentInsetAdjustmentBehavior='automatic'
data={albumsInfiniteQuery.data ?? []}
keyExtractor={(item) =>
@@ -102,10 +99,12 @@ export default function Albums({
backgroundColor={'$background'}
borderRadius={'$5'}
borderWidth={'$1'}
borderColor={'$borderColor'}
borderColor={'$primary'}
margin={'$2'}
>
<Text>{album.toUpperCase()}</Text>
<Text bold color={'$primary'}>
{album.toUpperCase()}
</Text>
</XStack>
) : typeof album === 'number' ? null : typeof album === 'object' ? (
<ItemRow

View File

@@ -97,13 +97,6 @@ export default function Artists({
<XStack flex={1}>
<FlashList
ref={sectionListRef}
style={{
width: getToken('$10'),
marginRight: getToken('$4'),
}}
contentContainerStyle={{
paddingTop: getToken('$3'),
}}
contentInsetAdjustmentBehavior='automatic'
extraData={isFavorites}
keyExtractor={(item) =>
@@ -160,7 +153,6 @@ export default function Artists({
artistsInfiniteQuery.fetchNextPage()
}}
// onEndReachedThreshold default is 0.5
removeClippedSubviews
/>
{showAlphabeticalSelector && artistPageParams && (

View File

@@ -38,7 +38,6 @@ export default function Index(): React.JSX.Element {
{suggestedArtistsInfiniteQuery.data && (
<View testID='discover-suggested-artists'>
<SuggestedArtists />
<Separator marginVertical={'$2'} />
</View>
)}
</ScrollView>

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { CardProps as TamaguiCardProps } from 'tamagui'
import { getToken, Card as TamaguiCard, View, YStack } from 'tamagui'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
@@ -22,7 +22,11 @@ interface CardProps extends TamaguiCardProps {
* @param props
*/
export function ItemCard(props: CardProps) {
const warmContext = useItemContext(props.item)
const warmContext = useItemContext()
useEffect(() => {
if (props.item.Type === 'Audio') warmContext(props.item)
}, [props.item])
return (
<View alignItems='center' margin={'$1.5'}>
@@ -35,7 +39,9 @@ export function ItemCard(props: CardProps) {
circular={!props.squared}
borderRadius={props.squared ? '$5' : 'unset'}
animation='bouncy'
onPressIn={warmContext}
onPressIn={() => {
if (props.item.Type !== 'Audio') warmContext(props.item)
}}
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
pressStyle={props.onPress ? { scale: 0.875 } : {}}
{...props}

View File

@@ -50,7 +50,7 @@ export default function ItemRow({
const { mutate: loadNewQueue } = useLoadNewQueue()
const warmContext = useItemContext(item)
const warmContext = useItemContext()
const gestureCallback = () => {
switch (item.Type) {
@@ -85,7 +85,7 @@ export default function ItemRow({
alignContent='center'
minHeight={'$7'}
width={'100%'}
onPressIn={warmContext}
onPressIn={() => warmContext(item)}
onLongPress={() => {
navigationRef.navigate('Context', {
item,

View File

@@ -69,8 +69,6 @@ export default function Track({
const offlineAudio = useDownloadedTrack(track.Id)
const warmContext = useItemContext(track)
// Memoize expensive computations
const isPlaying = useMemo(
() => nowPlaying?.item.Id === track.Id,
@@ -120,7 +118,7 @@ export default function Track({
downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo,
})
}
}, [onLongPress, track, isNested, offlineAudio])
}, [onLongPress, track, isNested, mediaInfo?.MediaSources, offlineAudio])
const handleIconPress = useCallback(() => {
if (showRemove) {
@@ -135,7 +133,7 @@ export default function Track({
downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo,
})
}
}, [showRemove, onRemove, track, isNested, offlineAudio])
}, [showRemove, onRemove, track, isNested, mediaInfo?.MediaSources, offlineAudio])
// Memoize text color to prevent recalculation
const textColor = useMemo(() => {
@@ -159,10 +157,6 @@ export default function Track({
[showArtwork, track.Artists],
)
useEffect(() => {
warmContext()
}, [track])
return (
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
<XStack
@@ -171,7 +165,6 @@ export default function Track({
height={showArtwork ? '$6' : '$5'}
flex={1}
testID={testID ?? undefined}
onPressIn={warmContext}
onPress={handlePress}
onLongPress={handleLongPress}
paddingVertical={'$2'}

View File

@@ -5,18 +5,18 @@ import { ItemCard } from '../../../components/Global/components/item-card'
import { useTheme, View, XStack } from 'tamagui'
import { H4, Text } from '../../../components/Global/helpers/text'
import Icon from '../../Global/components/icon'
import { useHomeContext } from '../../../providers/Home'
import { ActivityIndicator } from 'react-native'
import { useDisplayContext } from '../../../providers/Display/display-provider'
import { useNavigation } from '@react-navigation/native'
import HomeStackParamList from '../../../screens/Home/types'
import { RootStackParamList } from '../../../screens/types'
import { useFrequentlyPlayedArtists } from '../../../api/queries/frequents'
export default function FrequentArtists(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const { frequentArtistsInfiniteQuery } = useHomeContext()
const frequentArtistsInfiniteQuery = useFrequentlyPlayedArtists()
const theme = useTheme()
const { horizontalItems } = useDisplayContext()

View File

@@ -1,5 +1,4 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useHomeContext } from '../../../providers/Home'
import { View, XStack } from 'tamagui'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { ItemCard } from '../../../components/Global/components/item-card'
@@ -14,6 +13,7 @@ import { RootStackParamList } from '../../../screens/types'
import { useJellifyContext } from '../../../providers'
import { useNetworkStatus } from '../../../stores/network'
import useStreamingDeviceProfile from '../../../stores/device-profile'
import { useFrequentlyPlayedTracks } from '../../../api/queries/frequents'
export default function FrequentlyPlayedTracks(): React.JSX.Element {
const { api } = useJellifyContext()
@@ -22,12 +22,7 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
const deviceProfile = useStreamingDeviceProfile()
const {
frequentlyPlayed,
fetchNextFrequentlyPlayed,
hasNextFrequentlyPlayed,
isFetchingFrequentlyPlayed,
} = useHomeContext()
const tracksInfiniteQuery = useFrequentlyPlayedTracks()
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
@@ -42,10 +37,7 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
alignItems='center'
onPress={() => {
navigation.navigate('MostPlayedTracks', {
tracks: frequentlyPlayed,
fetchNextPage: fetchNextFrequentlyPlayed,
hasNextPage: hasNextFrequentlyPlayed,
isPending: isFetchingFrequentlyPlayed,
tracksInfiniteQuery,
})
}}
>
@@ -55,9 +47,9 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
<HorizontalCardList
data={
(frequentlyPlayed?.length ?? 0 > horizontalItems)
? frequentlyPlayed?.slice(0, horizontalItems)
: frequentlyPlayed
(tracksInfiniteQuery.data?.length ?? 0 > horizontalItems)
? tracksInfiniteQuery.data?.slice(0, horizontalItems)
: tracksInfiniteQuery.data
}
renderItem={({ item: track, index }) => (
<ItemCard
@@ -73,7 +65,7 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
networkStatus,
track,
index,
tracklist: frequentlyPlayed ?? [track],
tracklist: tracksInfiniteQuery.data ?? [track],
queue: 'On Repeat',
queuingType: QueuingType.FromSelection,
startPlayback: true,

View File

@@ -1,8 +1,7 @@
import React from 'react'
import { View, XStack } from 'tamagui'
import { useHomeContext } from '../../../providers/Home'
import { H4, Text } from '../../Global/helpers/text'
import { BaseStackParamList, RootStackParamList } from '../../../screens/types'
import { RootStackParamList } from '../../../screens/types'
import { ItemCard } from '../../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
@@ -11,9 +10,10 @@ import { useDisplayContext } from '../../../providers/Display/display-provider'
import { ActivityIndicator } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import HomeStackParamList from '../../../screens/Home/types'
import { useRecentArtists } from '../../../api/queries/recents'
export default function RecentArtists(): React.JSX.Element {
const { recentArtistsInfiniteQuery } = useHomeContext()
const recentArtistsInfiniteQuery = useRecentArtists()
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()

View File

@@ -1,6 +1,5 @@
import React, { useMemo } from 'react'
import { View, XStack } from 'tamagui'
import { useHomeContext } from '../../../providers/Home'
import { H4 } from '../../Global/helpers/text'
import { ItemCard } from '../../Global/components/item-card'
import { RootStackParamList } from '../../../screens/types'
@@ -16,6 +15,7 @@ import { useNowPlaying } from '../../../providers/Player/hooks/queries'
import { useJellifyContext } from '../../../providers'
import { useNetworkStatus } from '../../../stores/network'
import useStreamingDeviceProfile from '../../../stores/device-profile'
import { useRecentlyPlayedTracks } from '../../../api/queries/recents'
export default function RecentlyPlayed(): React.JSX.Element {
const { api } = useJellifyContext()
@@ -31,8 +31,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
const { mutate: loadNewQueue } = useLoadNewQueue()
const { recentTracks, fetchNextRecentTracks, hasNextRecentTracks, isFetchingRecentTracks } =
useHomeContext()
const tracksInfiniteQuery = useRecentlyPlayedTracks()
const { horizontalItems } = useDisplayContext()
return useMemo(() => {
@@ -42,10 +41,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
alignItems='center'
onPress={() => {
navigation.navigate('RecentTracks', {
tracks: recentTracks,
fetchNextPage: fetchNextRecentTracks,
hasNextPage: hasNextRecentTracks,
isPending: isFetchingRecentTracks,
tracksInfiniteQuery,
})
}}
>
@@ -55,9 +51,9 @@ export default function RecentlyPlayed(): React.JSX.Element {
<HorizontalCardList
data={
(recentTracks?.length ?? 0 > horizontalItems)
? recentTracks?.slice(0, horizontalItems)
: recentTracks
(tracksInfiniteQuery.data?.length ?? 0 > horizontalItems)
? tracksInfiniteQuery.data?.slice(0, horizontalItems)
: tracksInfiniteQuery.data
}
renderItem={({ index, item: recentlyPlayedTrack }) => (
<ItemCard
@@ -74,7 +70,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
networkStatus,
track: recentlyPlayedTrack,
index: index,
tracklist: recentTracks ?? [recentlyPlayedTrack],
tracklist: tracksInfiniteQuery.data ?? [recentlyPlayedTrack],
queue: 'Recently Played',
queuingType: QueuingType.FromSelection,
startPlayback: true,
@@ -91,5 +87,5 @@ export default function RecentlyPlayed(): React.JSX.Element {
/>
</View>
)
}, [recentTracks, nowPlaying])
}, [tracksInfiniteQuery.data, nowPlaying])
}

View File

@@ -2,15 +2,16 @@ import { ScrollView, RefreshControl } from 'react-native'
import { YStack, Separator, getToken } from 'tamagui'
import RecentArtists from './helpers/recent-artists'
import RecentlyPlayed from './helpers/recently-played'
import { useHomeContext } from '../../providers/Home'
import FrequentArtists from './helpers/frequent-artists'
import FrequentlyPlayedTracks from './helpers/frequent-tracks'
import { usePreventRemove } from '@react-navigation/native'
import { SafeAreaView } from 'react-native-safe-area-context'
import useHomeQueries from '../../api/queries/home'
export function ProvidedHome(): React.JSX.Element {
usePreventRemove(true, () => {})
const { refreshing: refetching, onRefresh } = useHomeContext()
const { data, isFetching: refreshing, refetch: refresh } = useHomeQueries()
return (
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
@@ -19,8 +20,7 @@ export function ProvidedHome(): React.JSX.Element {
contentContainerStyle={{
marginVertical: getToken('$4'),
}}
refreshControl={<RefreshControl refreshing={refetching} onRefresh={onRefresh} />}
removeClippedSubviews // Save memory usage
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}
>
<YStack alignContent='flex-start'>
<RecentArtists />

View File

@@ -1,13 +1,9 @@
import { useUserPlaylists } from '../../../api/queries/playlist'
import Playlists from '../../Playlists/component'
import React from 'react'
import { usePlaylistsInfiniteQueryContext } from '../../../providers/Library'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useNavigation } from '@react-navigation/native'
import LibraryStackParamList from '@/src/screens/Library/types'
import DiscoverStackParamList from '@/src/screens/Discover/types'
function PlaylistsTab(): React.JSX.Element {
const playlistsInfiniteQuery = usePlaylistsInfiniteQueryContext()
const playlistsInfiniteQuery = useUserPlaylists()
return (
<Playlists

View File

@@ -1,14 +1,14 @@
import React from 'react'
import Tracks from '../../Tracks/component'
import { useTracksInfiniteQueryContext } from '../../../providers/Library'
import { useLibrarySortAndFilterContext } from '../../../providers/Library/sorting-filtering'
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
import { useNavigation } from '@react-navigation/native'
import LibraryStackParamList from '@/src/screens/Library/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import useTracks from '../../../api/queries/track'
function TracksTab(): React.JSX.Element {
const tracksInfiniteQuery = useTracksInfiniteQueryContext()
const [trackPageParams, tracksInfiniteQuery] = useTracks()
const { isFavorites, isDownloaded } = useLibrarySortAndFilterContext()
@@ -17,12 +17,10 @@ function TracksTab(): React.JSX.Element {
return (
<Tracks
navigation={navigation}
tracks={tracksInfiniteQuery.data}
tracksInfiniteQuery={tracksInfiniteQuery}
queue={isFavorites ? 'Favorite Tracks' : isDownloaded ? 'Downloaded Tracks' : 'Library'}
filterDownloaded={isDownloaded}
filterFavorites={isFavorites}
fetchNextPage={tracksInfiniteQuery.fetchNextPage}
hasNextPage={tracksInfiniteQuery.hasNextPage}
showAlphabeticalSelector={true}
trackPageParams={trackPageParams}
/>
)
}

View File

@@ -2,7 +2,7 @@ import { MaterialTopTabBar, MaterialTopTabBarProps } from '@react-navigation/mat
import React from 'react'
import { XStack, YStack } from 'tamagui'
import Icon from '../Global/components/icon'
import { useLibrarySortAndFilterContext } from '../../providers/Library/sorting-filtering'
import { useLibrarySortAndFilterContext } from '../../providers/Library'
import { Text } from '../Global/helpers/text'
import { isUndefined } from 'lodash'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

View File

@@ -103,7 +103,6 @@ export default function Playlist({
marginHorizontal: 2,
}}
onScroll={scrollOffsetHandler}
removeClippedSubviews
/>
)
}

View File

@@ -1,95 +1,207 @@
import React from 'react'
import React, { RefObject, useMemo, useRef, useCallback, useEffect } from 'react'
import Track from '../Global/components/track'
import { getTokens, Separator } from 'tamagui'
import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getToken, Separator, XStack, YStack } from 'tamagui'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { Queue } from '../../player/types/queue-item'
import { queryClient } from '../../constants/query-client'
import { FlashList } from '@shopify/flash-list'
import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../screens/types'
import { useAllDownloadedTracks } from '../../api/queries/download'
import UserDataQueryKey from '../../api/queries/user-data/keys'
import { useJellifyContext } from '../../providers'
import { Text } from '../Global/helpers/text'
import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector'
import { UseInfiniteQueryResult } from '@tanstack/react-query'
import { debounce, isString } from 'lodash'
import { RefreshControl } from 'react-native-gesture-handler'
import useItemContext from '../../hooks/use-item-context'
interface TracksProps {
tracks: (string | number | BaseItemDto)[] | undefined
tracksInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
trackPageParams?: RefObject<Set<string>>
showAlphabeticalSelector?: boolean
navigation: Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
queue: Queue
fetchNextPage: () => void
hasNextPage: boolean
filterDownloaded?: boolean | undefined
filterFavorites?: boolean | undefined
}
export default function Tracks({
tracks,
tracksInfiniteQuery,
trackPageParams,
showAlphabeticalSelector,
navigation,
queue,
fetchNextPage,
hasNextPage,
filterDownloaded,
filterFavorites,
}: TracksProps): React.JSX.Element {
const { user } = useJellifyContext()
const warmContext = useItemContext()
const { data: downloadedTracks } = useAllDownloadedTracks()
const sectionListRef = useRef<FlashListRef<string | number | BaseItemDto>>(null)
const pendingLetterRef = useRef<string | null>(null)
const stickyHeaderIndicies = useMemo(() => {
if (!showAlphabeticalSelector || !tracksInfiniteQuery.data) return []
return tracksInfiniteQuery.data
.map((track, index) => (typeof track === 'string' ? index : 0))
.filter((value, index, indices) => indices.indexOf(value) === index)
}, [showAlphabeticalSelector, tracksInfiniteQuery.data])
const { mutate: alphabetSelectorMutate } = useAlphabetSelector(
(letter) => (pendingLetterRef.current = letter.toUpperCase()),
)
/**
* Warms the context for each visible item
*
* This is debounced, as to not fire all the time while the
* user is simply scrolling down the list of tracks
*/
const onViewableItemsChanged = useMemo(
() =>
debounce(
({
viewableItems,
}: {
viewableItems: ViewToken<string | number | BaseItemDto>[]
}) => {
viewableItems.forEach(({ isViewable, item: track }) => {
if (isViewable && typeof track === 'object') warmContext(track)
})
},
500,
{
leading: false,
trailing: true,
},
),
[],
)
// Memoize the expensive tracks processing to prevent memory leaks
const tracksToDisplay = React.useMemo(() => {
if (filterDownloaded) {
return (
downloadedTracks
?.map((downloadedTrack) => downloadedTrack.item)
.filter((downloadedTrack) => {
if (filterFavorites) {
return (
(
queryClient.getQueryData(
UserDataQueryKey(user!, downloadedTrack),
) as UserItemDataDto | undefined
)?.IsFavorite ?? false
)
}
return true
}) ?? []
)
}
return tracks?.filter((track) => typeof track === 'object') ?? []
}, [filterDownloaded, downloadedTracks, tracks, filterFavorites])
const tracksToDisplay = React.useMemo(
() => tracksInfiniteQuery.data?.filter((track) => typeof track === 'object') ?? [],
[tracksInfiniteQuery.data],
)
// Memoize key extraction for FlashList performance
const keyExtractor = React.useCallback((item: BaseItemDto) => item.Id!, [])
const keyExtractor = React.useCallback(
(item: string | number | BaseItemDto) =>
typeof item === 'object' ? item.Id! : item.toString(),
[],
)
// Memoize render item to prevent recreation
const renderItem = React.useCallback(
({ index, item: track }: { index: number; item: BaseItemDto }) => (
<Track
navigation={navigation}
showArtwork
index={0}
track={track}
tracklist={tracksToDisplay.slice(index, index + 50)}
queue={queue}
/>
),
/**
* Memoize render item to prevent recreation
*
* We're intentionally ignoring the item index here because
* it factors in the list headings, meaning pressing a track may not
* play that exact track, since the index was offset by the headings
*/
const renderItem = useCallback(
({ item: track }: { index: number; item: string | number | BaseItemDto }) =>
typeof track === 'string' ? (
<XStack
padding={'$2'}
backgroundColor={'$background'}
borderRadius={'$5'}
borderWidth={'$1'}
borderColor={'$primary'}
margin={'$2'}
>
<Text bold color={'$primary'}>
{track.toUpperCase()}
</Text>
</XStack>
) : typeof track === 'number' ? null : typeof track === 'object' ? (
<Track
navigation={navigation}
showArtwork
index={0}
track={track}
tracklist={tracksToDisplay.slice(
tracksToDisplay.indexOf(track),
tracksToDisplay.indexOf(track) + 50,
)}
queue={queue}
/>
) : null,
[tracksToDisplay, queue],
)
// Effect for handling the pending alphabet selector letter
useEffect(() => {
if (isString(pendingLetterRef.current) && tracksInfiniteQuery.data) {
const upperLetters = tracksInfiniteQuery.data
.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 = tracksInfiniteQuery.data.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 = tracksInfiniteQuery.data.indexOf(lastLetter)
if (scrollIndex !== -1) {
sectionListRef.current?.scrollToIndex({
index: scrollIndex,
viewPosition: 0.1,
animated: true,
})
}
}
pendingLetterRef.current = null
}
}, [pendingLetterRef.current, tracksInfiniteQuery.data])
return (
<FlashList
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
paddingVertical: getTokens().size.$1.val,
}}
ItemSeparatorComponent={() => <Separator />}
numColumns={1}
data={tracksToDisplay}
keyExtractor={keyExtractor}
renderItem={renderItem}
onEndReached={() => {
if (hasNextPage) fetchNextPage()
}}
removeClippedSubviews
/>
<XStack flex={1}>
<FlashList
ref={sectionListRef}
contentInsetAdjustmentBehavior='automatic'
ItemSeparatorComponent={() => <Separator />}
numColumns={1}
data={tracksInfiniteQuery.data}
keyExtractor={keyExtractor}
renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={tracksInfiniteQuery.isFetching}
onRefresh={tracksInfiniteQuery.refetch}
/>
}
onEndReached={() => {
if (tracksInfiniteQuery.hasNextPage) tracksInfiniteQuery.fetchNextPage()
}}
stickyHeaderIndices={stickyHeaderIndicies}
ListEmptyComponent={
<YStack flex={1} justify='center' alignItems='center'>
<Text marginVertical='auto' color={'$borderColor'}>
No tracks
</Text>
</YStack>
}
onViewableItemsChanged={onViewableItemsChanged}
/>
{showAlphabeticalSelector && trackPageParams && (
<AZScroller
onLetterSelect={(letter) =>
alphabetSelectorMutate({
letter,
infiniteQuery: tracksInfiniteQuery,
pageParams: trackPageParams,
})
}
/>
)}
</XStack>
)
}

View File

@@ -1,5 +1,3 @@
import { LibraryProvider } from '../providers/Library'
/**
* An enum of all the keys of query functions.
*/
@@ -72,7 +70,6 @@ export enum QueryKeys {
RecentlyAdded = 'RecentlyAdded',
SimilarItems = 'SimilarItems',
AudioCache = 'AudioCache',
MediaSources = 'MediaSources',
FrequentArtists = 'FrequentArtists',
FrequentlyPlayed = 'FrequentlyPlayed',
InstantMix = 'InstantMix',

View File

@@ -11,8 +11,9 @@ import { useJellifyContext } from '../providers'
import { useCallback, useRef } from 'react'
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../stores/device-profile'
import UserDataQueryKey from '../api/queries/user-data/keys'
import MediaInfoQueryKey from '../api/queries/media/keys'
export default function useItemContext(item: BaseItemDto): () => void {
export default function useItemContext(): (item: BaseItemDto) => void {
const { api, user } = useJellifyContext()
const streamingDeviceProfile = useStreamingDeviceProfile()
@@ -21,17 +22,20 @@ export default function useItemContext(item: BaseItemDto): () => void {
const prefetchedContext = useRef<Set<string>>(new Set())
return useCallback(() => {
const effectSig = `${item.Id}-${item.Type}`
return useCallback(
(item: BaseItemDto) => {
const effectSig = `${item.Id}-${item.Type}`
// If we've already warmed the cache for this item, return
if (prefetchedContext.current.has(effectSig)) return
// If we've already warmed the cache for this item, return
if (prefetchedContext.current.has(effectSig)) return
// Mark this item's context as warmed, preventing reruns
prefetchedContext.current.add(effectSig)
// Mark this item's context as warmed, preventing reruns
prefetchedContext.current.add(effectSig)
warmItemContext(api, user, item, streamingDeviceProfile, downloadingDeviceProfile)
}, [api, user, streamingDeviceProfile])
warmItemContext(api, user, item, streamingDeviceProfile, downloadingDeviceProfile)
},
[api, user, streamingDeviceProfile],
)
}
function warmItemContext(
@@ -49,7 +53,7 @@ function warmItemContext(
console.debug(`Warming context query cache for item ${Id}`)
if (Type === BaseItemKind.Audio)
warmTrackContext(api, user, item, streamingDeviceProfile, downloadingDeviceProfile)
warmTrackContext(api, item, streamingDeviceProfile, downloadingDeviceProfile)
if (Type === BaseItemKind.MusicArtist)
queryClient.setQueryData([QueryKeys.ArtistById, Id], item)
@@ -118,31 +122,36 @@ function warmArtistContext(api: Api | undefined, artistId: string): void {
function warmTrackContext(
api: Api | undefined,
user: JellifyUser | undefined,
track: BaseItemDto,
streamingDeviceProfile: DeviceProfile | undefined,
downloadingDeviceProfile: DeviceProfile | undefined,
): void {
const { Id, AlbumId, ArtistItems } = track
const streamingMediaSourceQueryKey = [QueryKeys.MediaSources, streamingDeviceProfile?.Name, Id]
if (queryClient.getQueryState(streamingMediaSourceQueryKey)?.status !== 'success')
if (
queryClient.getQueryState(
MediaInfoQueryKey({ api, deviceProfile: streamingDeviceProfile, itemId: Id! }),
)?.status !== 'success'
)
queryClient.ensureQueryData({
queryKey: streamingMediaSourceQueryKey,
queryFn: () => fetchMediaInfo(api, user, streamingDeviceProfile, Id!),
queryKey: MediaInfoQueryKey({
api,
deviceProfile: streamingDeviceProfile,
itemId: Id!,
}),
queryFn: () => fetchMediaInfo(api, streamingDeviceProfile, Id!),
})
const downloadedMediaSourceQueryKey = [
QueryKeys.MediaSources,
downloadingDeviceProfile?.Name,
Id,
]
const downloadedMediaSourceQueryKey = MediaInfoQueryKey({
api,
deviceProfile: downloadingDeviceProfile,
itemId: Id!,
})
if (queryClient.getQueryState(downloadedMediaSourceQueryKey)?.status !== 'success')
queryClient.ensureQueryData({
queryKey: downloadedMediaSourceQueryKey,
queryFn: () => fetchMediaInfo(api, user, downloadingDeviceProfile, track.Id),
queryFn: () => fetchMediaInfo(api, downloadingDeviceProfile, track.Id),
})
const albumQueryKey = [QueryKeys.Album, AlbumId]

View File

@@ -4,27 +4,21 @@ import {
useInfiniteQuery,
UseInfiniteQueryResult,
} from '@tanstack/react-query'
import { fetchRecentlyPlayed } from '../../api/queries/recents'
import { QueryKeys } from '../../enums/query-keys'
import { createContext, ReactNode, useContext, useState } from 'react'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { useJellifyContext } from '..'
import { fetchPublicPlaylists } from '../../api/queries/playlists'
import { fetchPublicPlaylists } from '../../api/queries/playlist/utils'
import { fetchArtistSuggestions } from '../../api/queries/suggestions'
import { useRefetchRecentlyAdded } from '../../api/queries/album'
interface DiscoverContext {
refreshing: boolean
refresh: () => void
recentlyPlayed: InfiniteData<BaseItemDto[], unknown> | undefined
publicPlaylists: BaseItemDto[] | undefined
fetchNextRecentlyPlayed: () => void
fetchNextPublicPlaylists: () => void
hasNextRecentlyPlayed: boolean
hasNextPublicPlaylists: boolean
isPendingRecentlyPlayed: boolean
isPendingPublicPlaylists: boolean
isFetchingNextRecentlyPlayed: boolean
isFetchingNextPublicPlaylists: boolean
refetchPublicPlaylists: () => void
suggestedArtistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
@@ -52,21 +46,6 @@ const DiscoverContextInitializer = () => {
initialPageParam: 0,
})
const {
data: recentlyPlayed,
refetch: refetchRecentlyPlayed,
fetchNextPage: fetchNextRecentlyPlayed,
hasNextPage: hasNextRecentlyPlayed,
isPending: isPendingRecentlyPlayed,
isFetchingNextPage: isFetchingNextRecentlyPlayed,
} = useInfiniteQuery({
queryKey: [QueryKeys.RecentlyPlayed, library?.musicLibraryId],
queryFn: ({ pageParam }) => fetchRecentlyPlayed(api, user, library, pageParam),
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
lastPage.length > 0 ? lastPageParam + 1 : undefined,
initialPageParam: 0,
})
const suggestedArtistsInfiniteQuery = useInfiniteQuery({
queryKey: [QueryKeys.InfiniteSuggestedArtists, user?.id, library?.musicLibraryId],
queryFn: ({ pageParam }) =>
@@ -83,7 +62,6 @@ const DiscoverContextInitializer = () => {
await Promise.all([
refetchRecentlyAdded(),
refetchRecentlyPlayed(),
refetchPublicPlaylists(),
suggestedArtistsInfiniteQuery.refetch(),
])
@@ -93,15 +71,10 @@ const DiscoverContextInitializer = () => {
return {
refreshing,
refresh,
recentlyPlayed,
publicPlaylists,
fetchNextRecentlyPlayed,
fetchNextPublicPlaylists,
hasNextRecentlyPlayed,
hasNextPublicPlaylists,
isPendingRecentlyPlayed,
isPendingPublicPlaylists,
isFetchingNextRecentlyPlayed,
isFetchingNextPublicPlaylists,
refetchPublicPlaylists,
suggestedArtistsInfiniteQuery,
@@ -111,15 +84,10 @@ const DiscoverContextInitializer = () => {
const DiscoverContext = createContext<DiscoverContext>({
refreshing: false,
refresh: () => {},
recentlyPlayed: undefined,
publicPlaylists: undefined,
fetchNextRecentlyPlayed: () => {},
fetchNextPublicPlaylists: () => {},
hasNextRecentlyPlayed: false,
hasNextPublicPlaylists: false,
isPendingRecentlyPlayed: false,
isPendingPublicPlaylists: false,
isFetchingNextRecentlyPlayed: false,
isFetchingNextPublicPlaylists: false,
refetchPublicPlaylists: () => {},
suggestedArtistsInfiniteQuery: {

View File

@@ -1,240 +0,0 @@
import React, { createContext, ReactNode, useCallback, useContext, useState } from 'react'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import {
InfiniteQueryObserverResult,
useInfiniteQuery,
UseInfiniteQueryResult,
} from '@tanstack/react-query'
import { QueryKeys } from '../../enums/query-keys'
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from '../../api/queries/recents'
import { queryClient } from '../../constants/query-client'
import QueryConfig from '../../api/queries/query.config'
import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from '../../api/queries/frequents'
import { useJellifyContext } from '..'
interface HomeContext {
refreshing: boolean
onRefresh: () => void
recentTracks: BaseItemDto[] | undefined
fetchNextRecentTracks: () => void
hasNextRecentTracks: boolean
fetchNextFrequentlyPlayed: () => void
hasNextFrequentlyPlayed: boolean
frequentlyPlayed: BaseItemDto[] | undefined
isFetchingRecentTracks: boolean
isFetchingFrequentlyPlayed: boolean
recentArtistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
frequentArtistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
const HomeContextInitializer = () => {
const { api, library, user } = useJellifyContext()
const [refreshing, setRefreshing] = useState<boolean>(false)
const {
data: recentTracks,
isFetching: isFetchingRecentTracks,
refetch: refetchRecentTracks,
isError: isErrorRecentTracks,
fetchNextPage: fetchNextRecentTracks,
hasNextPage: hasNextRecentTracks,
isPending: isPendingRecentTracks,
isStale: isStaleRecentTracks,
} = useInfiniteQuery({
queryKey: [QueryKeys.RecentlyPlayed, library?.musicLibraryId],
queryFn: ({ pageParam }) => fetchRecentlyPlayed(api, user, library, pageParam),
initialPageParam: 0,
select: (data) => data.pages.flatMap((page) => page),
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for recent tracks')
return lastPage.length === QueryConfig.limits.recents ? lastPageParam + 1 : undefined
},
})
const recentArtistsInfiniteQuery = useInfiniteQuery({
queryKey: [QueryKeys.RecentlyPlayedArtists, library?.musicLibraryId],
queryFn: ({ pageParam }) => fetchRecentlyPlayedArtists(api, user, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for recent artists')
return lastPage.length > 0 ? lastPageParam + 1 : undefined
},
enabled: !!recentTracks && recentTracks.length > 0 && !isPendingRecentTracks,
})
const {
data: frequentlyPlayed,
isFetching: isFetchingFrequentlyPlayed,
refetch: refetchFrequentlyPlayed,
fetchNextPage: fetchNextFrequentlyPlayed,
hasNextPage: hasNextFrequentlyPlayed,
isPending: isPendingFrequentlyPlayed,
isStale: isStaleFrequentlyPlayed,
} = useInfiniteQuery({
queryKey: [QueryKeys.FrequentlyPlayed, library?.musicLibraryId],
queryFn: ({ pageParam }) => fetchFrequentlyPlayed(api, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for frequently played')
return lastPage.length === QueryConfig.limits.recents ? lastPageParam + 1 : undefined
},
})
const frequentArtistsInfiniteQuery = useInfiniteQuery({
queryKey: [QueryKeys.FrequentArtists, library?.musicLibraryId],
queryFn: ({ pageParam }) => fetchFrequentlyPlayedArtists(api, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for frequent artists')
return lastPage.length === 100 ? lastPageParam + 1 : undefined
},
enabled: !!frequentlyPlayed && frequentlyPlayed.length > 0 && !isStaleFrequentlyPlayed,
})
const onRefresh = useCallback(async () => {
setRefreshing(true)
queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyPlayedArtists] })
queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyPlayed] })
queryClient.invalidateQueries({ queryKey: [QueryKeys.FrequentlyPlayed] })
queryClient.invalidateQueries({ queryKey: [QueryKeys.FrequentArtists] })
await Promise.all([refetchRecentTracks(), refetchFrequentlyPlayed()])
await Promise.all([
recentArtistsInfiniteQuery.refetch(),
frequentArtistsInfiniteQuery.refetch(),
])
setRefreshing(false)
}, [
refetchRecentTracks,
refetchFrequentlyPlayed,
recentArtistsInfiniteQuery.refetch,
frequentArtistsInfiniteQuery.refetch,
])
return {
refreshing,
onRefresh,
recentTracks,
recentArtistsInfiniteQuery,
frequentArtistsInfiniteQuery,
isFetchingRecentTracks,
isFetchingFrequentlyPlayed,
fetchNextRecentTracks,
hasNextRecentTracks,
fetchNextFrequentlyPlayed,
hasNextFrequentlyPlayed,
frequentlyPlayed,
}
}
const HomeContext = createContext<HomeContext>({
refreshing: false,
onRefresh: () => {},
recentTracks: undefined,
frequentlyPlayed: undefined,
isFetchingRecentTracks: false,
isFetchingFrequentlyPlayed: false,
recentArtistsInfiniteQuery: {
data: undefined,
error: null,
isEnabled: true,
isStale: false,
isRefetching: false,
isError: false,
isLoading: true,
isPending: true,
isFetching: true,
isSuccess: false,
isFetched: false,
hasPreviousPage: false,
refetch: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
fetchNextPage: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
hasNextPage: false,
isFetchingNextPage: false,
isFetchingPreviousPage: false,
isFetchPreviousPageError: false,
isFetchNextPageError: false,
isLoadingError: false,
isRefetchError: false,
isPlaceholderData: false,
status: 'pending',
fetchStatus: 'idle',
dataUpdatedAt: 0,
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetchedAfterMount: false,
isInitialLoading: false,
isPaused: false,
fetchPreviousPage: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
promise: Promise.resolve([]),
},
frequentArtistsInfiniteQuery: {
data: undefined,
error: null,
isEnabled: true,
isStale: false,
isRefetching: false,
isError: false,
isLoading: true,
isPending: true,
isFetching: true,
isSuccess: false,
isFetched: false,
hasPreviousPage: false,
refetch: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
fetchNextPage: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
hasNextPage: false,
isFetchingNextPage: false,
isFetchingPreviousPage: false,
isFetchPreviousPageError: false,
isFetchNextPageError: false,
isLoadingError: false,
isRefetchError: false,
isPlaceholderData: false,
status: 'pending',
fetchStatus: 'idle',
dataUpdatedAt: 0,
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetchedAfterMount: false,
isInitialLoading: false,
isPaused: false,
fetchPreviousPage: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
promise: Promise.resolve([]),
},
fetchNextRecentTracks: () => {},
hasNextRecentTracks: false,
fetchNextFrequentlyPlayed: () => {},
hasNextFrequentlyPlayed: false,
})
export const HomeProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
children,
}: {
children: ReactNode
}) => {
const context = HomeContextInitializer()
return <HomeContext.Provider value={context}>{children}</HomeContext.Provider>
}
export const useHomeContext = () => useContext(HomeContext)

View File

@@ -1,179 +1,60 @@
import { QueryKeys } from '../../enums/query-keys'
import { BaseItemDto, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
import { useJellifyContext } from '..'
import { useMemo } from 'react'
import QueryConfig from '../../api/queries/query.config'
import { fetchTracks } from '../../api/queries/tracks'
import { useLibrarySortAndFilterContext } from './sorting-filtering'
import { fetchUserPlaylists } from '../../api/queries/playlists'
import { createContext, useContextSelector } from 'use-context-selector'
import {
InfiniteQueryObserverResult,
useInfiniteQuery,
UseInfiniteQueryResult,
} from '@tanstack/react-query'
import { storage } from '../../constants/storage'
import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
import { useContext, useEffect, useState } from 'react'
import { createContext } from 'react'
export const alphabet = '#abcdefghijklmnopqrstuvwxyz'.split('')
interface LibraryContext {
tracksInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
// genres: BaseItemDto[] | undefined
playlistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
interface LibrarySortAndFilterContext {
sortDescending: boolean
setSortDescending: (sortDescending: boolean) => void
isFavorites: boolean | undefined
setIsFavorites: (isFavorites: boolean | undefined) => void
isDownloaded: boolean
setIsDownloaded: (isDownloaded: boolean) => void
}
type LibraryPage = {
title: string
data: BaseItemDto[]
}
const LibrarySortAndFilterContextInitializer = () => {
const sortDescendingInit = storage.getBoolean(MMKVStorageKeys.LibrarySortDescending)
const isFavoritesInit = storage.getBoolean(MMKVStorageKeys.LibraryIsFavorites)
const isDownloadedInit = storage.getBoolean(MMKVStorageKeys.LibraryIsDownloaded)
const LibraryContextInitializer = () => {
const { api, user, library } = useJellifyContext()
const [sortDescending, setSortDescending] = useState(sortDescendingInit ?? false)
const [isFavorites, setIsFavorites] = useState<boolean | undefined>(isFavoritesInit)
const [isDownloaded, setIsDownloaded] = useState(isDownloadedInit ?? false)
const { sortDescending, isFavorites } = useLibrarySortAndFilterContext()
useEffect(() => {
storage.set(MMKVStorageKeys.LibrarySortDescending, sortDescending)
storage.set(MMKVStorageKeys.LibraryIsDownloaded, isDownloaded)
const tracksInfiniteQuery = useInfiniteQuery({
queryKey: [QueryKeys.AllTracks, isFavorites, sortDescending, library?.musicLibraryId],
queryFn: ({ pageParam }) =>
fetchTracks(
api,
user,
library,
pageParam,
isFavorites,
ItemSortBy.SortName,
sortDescending ? SortOrder.Descending : SortOrder.Ascending,
),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug(`Tracks last page length: ${lastPage.length}`)
return lastPage.length === QueryConfig.limits.library * 2
? lastPageParam + 1
: undefined
},
select: (data) => data.pages.flatMap((page) => page),
})
const playlistsInfiniteQuery = useInfiniteQuery({
queryKey: [QueryKeys.Playlists, library?.playlistLibraryId],
queryFn: () => fetchUserPlaylists(api, user, library),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined
},
})
if (isFavorites !== undefined) storage.set(MMKVStorageKeys.LibraryIsFavorites, isFavorites)
else storage.delete(MMKVStorageKeys.LibraryIsFavorites)
}, [sortDescending, isFavorites, isDownloaded])
return {
tracksInfiniteQuery,
playlistsInfiniteQuery,
sortDescending,
setSortDescending,
isFavorites,
setIsFavorites,
isDownloaded,
setIsDownloaded,
}
}
const LibraryContext = createContext<LibraryContext>({
tracksInfiniteQuery: {
data: undefined,
error: null,
isEnabled: true,
isStale: false,
isRefetching: false,
isError: false,
isLoading: true,
isPending: true,
isFetching: true,
isSuccess: false,
isFetched: false,
hasPreviousPage: false,
refetch: async () =>
Promise.resolve(
{} as InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>,
),
fetchNextPage: async () =>
Promise.resolve(
{} as InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>,
),
hasNextPage: false,
isFetchingNextPage: false,
isFetchPreviousPageError: false,
isFetchNextPageError: false,
isFetchingPreviousPage: false,
isLoadingError: false,
isRefetchError: false,
isPlaceholderData: false,
status: 'pending',
fetchStatus: 'idle',
dataUpdatedAt: 0,
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetchedAfterMount: false,
isInitialLoading: false,
isPaused: false,
fetchPreviousPage: async () =>
Promise.resolve(
{} as InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>,
),
promise: Promise.resolve([]),
},
playlistsInfiniteQuery: {
data: undefined,
error: null,
isEnabled: true,
isStale: false,
isRefetching: false,
isError: false,
isLoading: true,
isPending: true,
isFetching: true,
isSuccess: false,
isFetched: false,
hasPreviousPage: false,
refetch: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
fetchNextPage: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
hasNextPage: false,
isFetchingNextPage: false,
isFetchPreviousPageError: false,
isFetchNextPageError: false,
isFetchingPreviousPage: false,
isLoadingError: false,
isRefetchError: false,
isPlaceholderData: false,
status: 'pending',
fetchStatus: 'idle',
dataUpdatedAt: 0,
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetchedAfterMount: false,
isInitialLoading: false,
isPaused: false,
fetchPreviousPage: async () =>
Promise.resolve({} as InfiniteQueryObserverResult<BaseItemDto[], Error>),
promise: Promise.resolve([]),
},
const LibrarySortAndFilterContext = createContext<LibrarySortAndFilterContext>({
sortDescending: false,
setSortDescending: () => {},
isFavorites: false,
setIsFavorites: () => {},
isDownloaded: false,
setIsDownloaded: () => {},
})
export const LibraryProvider = ({ children }: { children: React.ReactNode }) => {
const context = LibraryContextInitializer()
export const LibrarySortAndFilterProvider = ({ children }: { children: React.ReactNode }) => {
const context = LibrarySortAndFilterContextInitializer()
const value = useMemo(
() => context,
[
context.tracksInfiniteQuery.data,
context.tracksInfiniteQuery.isPending,
context.playlistsInfiniteQuery.data,
context.playlistsInfiniteQuery.isPending,
],
return (
<LibrarySortAndFilterContext.Provider value={context}>
{children}
</LibrarySortAndFilterContext.Provider>
)
return <LibraryContext.Provider value={value}>{children}</LibraryContext.Provider>
}
export const useTracksInfiniteQueryContext = () =>
useContextSelector(LibraryContext, (context) => context.tracksInfiniteQuery)
export const usePlaylistsInfiniteQueryContext = () =>
useContextSelector(LibraryContext, (context) => context.playlistsInfiniteQuery)
export { useLibrarySortAndFilterContext }
export const useLibrarySortAndFilterContext = () => useContext(LibrarySortAndFilterContext)

View File

@@ -1,60 +0,0 @@
import { storage } from '../../constants/storage'
import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
import { useContext, useEffect, useState } from 'react'
import { createContext } from 'react'
interface LibrarySortAndFilterContext {
sortDescending: boolean
setSortDescending: (sortDescending: boolean) => void
isFavorites: boolean | undefined
setIsFavorites: (isFavorites: boolean | undefined) => void
isDownloaded: boolean
setIsDownloaded: (isDownloaded: boolean) => void
}
const LibrarySortAndFilterContextInitializer = () => {
const sortDescendingInit = storage.getBoolean(MMKVStorageKeys.LibrarySortDescending)
const isFavoritesInit = storage.getBoolean(MMKVStorageKeys.LibraryIsFavorites)
const isDownloadedInit = storage.getBoolean(MMKVStorageKeys.LibraryIsDownloaded)
const [sortDescending, setSortDescending] = useState(sortDescendingInit ?? false)
const [isFavorites, setIsFavorites] = useState<boolean | undefined>(isFavoritesInit)
const [isDownloaded, setIsDownloaded] = useState(isDownloadedInit ?? false)
useEffect(() => {
storage.set(MMKVStorageKeys.LibrarySortDescending, sortDescending)
storage.set(MMKVStorageKeys.LibraryIsDownloaded, isDownloaded)
if (isFavorites !== undefined) storage.set(MMKVStorageKeys.LibraryIsFavorites, isFavorites)
else storage.delete(MMKVStorageKeys.LibraryIsFavorites)
}, [sortDescending, isFavorites, isDownloaded])
return {
sortDescending,
setSortDescending,
isFavorites,
setIsFavorites,
isDownloaded,
setIsDownloaded,
}
}
const LibrarySortAndFilterContext = createContext<LibrarySortAndFilterContext>({
sortDescending: false,
setSortDescending: () => {},
isFavorites: false,
setIsFavorites: () => {},
isDownloaded: false,
setIsDownloaded: () => {},
})
export const LibrarySortAndFilterProvider = ({ children }: { children: React.ReactNode }) => {
const context = LibrarySortAndFilterContextInitializer()
return (
<LibrarySortAndFilterContext.Provider value={context}>
{children}
</LibrarySortAndFilterContext.Provider>
)
}
export const useLibrarySortAndFilterContext = () => useContext(LibrarySortAndFilterContext)

View File

@@ -1,8 +1,7 @@
import { createContext } from 'use-context-selector'
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
import { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
import { refetchNowPlaying } from './functions/queries'
import { useEffect } from 'react'
import { createContext, useEffect } from 'react'
import { useAudioNormalization, useInitialization } from './hooks/mutations'
import { useCurrentIndex, useNowPlaying, useQueue } from './hooks/queries'
import { handleActiveTrackChanged } from './functions'

View File

@@ -1,13 +1,15 @@
import React from 'react'
import Artists from '../../components/Artists/component'
import { MostPlayedArtistsProps, RecentArtistsProps } from './types'
import { useHomeContext } from '../../providers/Home'
import { useRecentArtists } from '../../api/queries/recents'
import { useFrequentlyPlayedArtists } from '../../api/queries/frequents'
export default function HomeArtistsScreen({
navigation,
route,
}: RecentArtistsProps | MostPlayedArtistsProps): React.JSX.Element {
const { recentArtistsInfiniteQuery, frequentArtistsInfiniteQuery } = useHomeContext()
const recentArtistsInfiniteQuery = useRecentArtists()
const frequentArtistsInfiniteQuery = useFrequentlyPlayedArtists()
if (route.name === 'MostPlayedArtists') {
return (

View File

@@ -1,5 +1,4 @@
import _ from 'lodash'
import { HomeProvider } from '../../providers/Home'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { PlaylistScreen } from '../Playlist'
import { ProvidedHome } from '../../components/Home'
@@ -9,7 +8,6 @@ import HomeArtistsScreen from './artists'
import HomeTracksScreen from './tracks'
import AlbumScreen from '../Album'
import HomeStackParamList from './types'
import { HomeTabProps } from '../Tabs/types'
import InstantMix from '../../components/InstantMix/component'
import { getItemName } from '../../utils/text'
@@ -23,86 +21,84 @@ export default function Home(): React.JSX.Element {
const theme = useTheme()
return (
<HomeProvider>
<HomeStack.Navigator initialRouteName='HomeScreen'>
<HomeStack.Group>
<HomeStack.Screen
name='HomeScreen'
component={ProvidedHome}
options={{
title: 'Home',
headerShown: false,
headerTitleStyle: {
fontFamily: 'Figtree-Bold',
},
}}
/>
<HomeStack.Screen
name='Artist'
component={ArtistScreen}
options={({ route }) => ({
title: route.params.artist.Name ?? 'Unknown Artist',
headerTitleStyle: {
color: theme.background.val,
fontFamily: 'Figtree-Bold',
},
})}
/>
<HomeStack.Navigator initialRouteName='HomeScreen'>
<HomeStack.Group>
<HomeStack.Screen
name='HomeScreen'
component={ProvidedHome}
options={{
title: 'Home',
headerShown: false,
headerTitleStyle: {
fontFamily: 'Figtree-Bold',
},
}}
/>
<HomeStack.Screen
name='Artist'
component={ArtistScreen}
options={({ route }) => ({
title: route.params.artist.Name ?? 'Unknown Artist',
headerTitleStyle: {
color: theme.background.val,
fontFamily: 'Figtree-Bold',
},
})}
/>
<HomeStack.Screen
name='RecentArtists'
component={HomeArtistsScreen}
options={{ title: 'Recent Artists' }}
/>
<HomeStack.Screen
name='MostPlayedArtists'
component={HomeArtistsScreen}
options={{ title: 'Most Played' }}
/>
<HomeStack.Screen
name='RecentArtists'
component={HomeArtistsScreen}
options={{ title: 'Recent Artists' }}
/>
<HomeStack.Screen
name='MostPlayedArtists'
component={HomeArtistsScreen}
options={{ title: 'Most Played' }}
/>
<HomeStack.Screen
name='RecentTracks'
component={HomeTracksScreen}
options={{ title: 'Recently Played' }}
/>
<HomeStack.Screen
name='RecentTracks'
component={HomeTracksScreen}
options={{ title: 'Recently Played' }}
/>
<HomeStack.Screen
name='MostPlayedTracks'
component={HomeTracksScreen}
options={{ title: 'On Repeat' }}
/>
<HomeStack.Screen
name='MostPlayedTracks'
component={HomeTracksScreen}
options={{ title: 'On Repeat' }}
/>
<HomeStack.Screen
name='Album'
component={AlbumScreen}
options={({ route }) => ({
title: route.params.album.Name ?? 'Untitled Album',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<HomeStack.Screen
name='Album'
component={AlbumScreen}
options={({ route }) => ({
title: route.params.album.Name ?? 'Untitled Album',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<HomeStack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
headerShown: false,
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<HomeStack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
headerShown: false,
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<HomeStack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
headerTitle: `${getItemName(route.params.item)} Mix`,
})}
/>
</HomeStack.Group>
</HomeStack.Navigator>
</HomeProvider>
<HomeStack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
headerTitle: `${getItemName(route.params.item)} Mix`,
})}
/>
</HomeStack.Group>
</HomeStack.Navigator>
)
}

View File

@@ -1,27 +1,21 @@
import { useRecentlyPlayedTracks } from '../../api/queries/recents'
import Tracks from '../../components/Tracks/component'
import { useHomeContext } from '../../providers/Home'
import { MostPlayedTracksProps, RecentTracksProps } from './types'
import { useFrequentlyPlayedTracks } from '../../api/queries/frequents'
export default function HomeTracksScreen({
navigation,
route,
}: RecentTracksProps | MostPlayedTracksProps): React.JSX.Element {
const {
recentTracks,
frequentlyPlayed,
fetchNextRecentTracks,
hasNextRecentTracks,
fetchNextFrequentlyPlayed,
hasNextFrequentlyPlayed,
} = useHomeContext()
const recentlyPlayedTracks = useRecentlyPlayedTracks()
const frequentlyPlayedTracks = useFrequentlyPlayedTracks()
if (route.name === 'MostPlayedTracks') {
return (
<Tracks
navigation={navigation}
tracks={frequentlyPlayed}
fetchNextPage={fetchNextFrequentlyPlayed}
hasNextPage={hasNextFrequentlyPlayed}
tracksInfiniteQuery={frequentlyPlayedTracks}
queue={'On Repeat'}
/>
)
@@ -30,9 +24,7 @@ export default function HomeTracksScreen({
return (
<Tracks
navigation={navigation}
tracks={recentTracks}
fetchNextPage={fetchNextRecentTracks}
hasNextPage={hasNextRecentTracks}
tracksInfiniteQuery={recentlyPlayedTracks}
queue={'Recently Played'}
/>
)

View File

@@ -14,16 +14,10 @@ type HomeStackParamList = BaseStackParamList & {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
RecentTracks: {
tracks: BaseItemDto[] | undefined
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
tracksInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
MostPlayedTracks: {
tracks: BaseItemDto[] | undefined
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
tracksInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
}

View File

@@ -6,13 +6,12 @@ import Button from '../../components/Global/helpers/button'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useMutation } from '@tanstack/react-query'
import { createPlaylist } from '../../api/mutations/playlists'
import { queryClient } from '../../constants/query-client'
import { QueryKeys } from '../../enums/query-keys'
import Toast from 'react-native-toast-message'
import { useJellifyContext } from '../../providers'
import Icon from '../../components/Global/components/icon'
import LibraryStackParamList from './types'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { useUserPlaylists } from '../../api/queries/playlist'
export default function AddPlaylist({
navigation,
@@ -22,6 +21,8 @@ export default function AddPlaylist({
const { api, user } = useJellifyContext()
const [name, setName] = useState<string>('')
const { refetch } = useUserPlaylists()
const trigger = useHapticFeedback()
const useAddPlaylist = useMutation({
@@ -44,9 +45,7 @@ export default function AddPlaylist({
navigation.goBack()
// Refresh user playlists component in library
queryClient.invalidateQueries({
queryKey: [QueryKeys.Playlists],
})
refetch()
},
onError: () => {
trigger('notificationError')

View File

@@ -9,8 +9,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
import AlbumScreen from '../Album'
import LibraryStackParamList from './types'
import { LibraryTabProps } from '../Tabs/types'
import { LibraryProvider } from '../../providers/Library'
import { LibrarySortAndFilterProvider } from '../../providers/Library/sorting-filtering'
import { LibrarySortAndFilterProvider } from '../../providers/Library'
import InstantMix from '../../components/InstantMix/component'
import { getItemName } from '../../utils/text'
@@ -21,88 +20,86 @@ export default function LibraryScreen({ route, navigation }: LibraryTabProps): R
return (
<LibrarySortAndFilterProvider>
<LibraryProvider>
<LibraryStack.Navigator initialRouteName='LibraryScreen'>
<LibraryStack.Navigator initialRouteName='LibraryScreen'>
<LibraryStack.Screen
name='LibraryScreen'
component={Library}
options={{
title: 'Library',
// I honestly don't think we need a header for this screen, given that there are
// tabs on the top of the screen for navigating the library, but if we want one,
// we can use the title above
headerShown: false,
}}
/>
<LibraryStack.Screen
name='Artist'
component={ArtistScreen}
options={({ route }) => ({
title: route.params.artist.Name ?? 'Unknown Artist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<LibraryStack.Screen
name='Album'
component={AlbumScreen}
options={({ route }) => ({
title: route.params.album.Name ?? 'Untitled Album',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<LibraryStack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
title: route.params.playlist.Name ?? 'Untitled Playlist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<LibraryStack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
headerTitle: `${getItemName(route.params.item)} Mix`,
})}
/>
<LibraryStack.Group
screenOptions={{
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
}}
>
<LibraryStack.Screen
name='LibraryScreen'
component={Library}
name='AddPlaylist'
component={AddPlaylist}
options={{
title: 'Library',
title: 'Add Playlist',
}}
/>
// I honestly don't think we need a header for this screen, given that there are
// tabs on the top of the screen for navigating the library, but if we want one,
// we can use the title above
<LibraryStack.Screen
name='DeletePlaylist'
component={DeletePlaylist}
options={{
title: 'Delete Playlist',
headerShown: false,
sheetGrabberVisible: true,
}}
/>
<LibraryStack.Screen
name='Artist'
component={ArtistScreen}
options={({ route }) => ({
title: route.params.artist.Name ?? 'Unknown Artist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<LibraryStack.Screen
name='Album'
component={AlbumScreen}
options={({ route }) => ({
title: route.params.album.Name ?? 'Untitled Album',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<LibraryStack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
title: route.params.playlist.Name ?? 'Untitled Playlist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<LibraryStack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
headerTitle: `${getItemName(route.params.item)} Mix`,
})}
/>
<LibraryStack.Group
screenOptions={{
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
}}
>
<LibraryStack.Screen
name='AddPlaylist'
component={AddPlaylist}
options={{
title: 'Add Playlist',
}}
/>
<LibraryStack.Screen
name='DeletePlaylist'
component={DeletePlaylist}
options={{
title: 'Delete Playlist',
headerShown: false,
sheetGrabberVisible: true,
}}
/>
</LibraryStack.Group>
</LibraryStack.Navigator>
</LibraryProvider>
</LibraryStack.Group>
</LibraryStack.Navigator>
</LibrarySortAndFilterProvider>
)
}

View File

@@ -1,16 +0,0 @@
import { NavigationProp } from '@react-navigation/native'
import LibraryStackParamList from './types'
import { createContext, useContext } from 'use-context-selector'
import TabParamList from '../Tabs/types'
export const LibraryNavigationContext = createContext<NavigationProp<TabParamList> | null>(null)
const useLibraryNavigation = () => {
const context = useContext(LibraryNavigationContext)
if (!context)
throw new Error('useLibraryNavigation must be used in the the LibraryNavigationProvider')
return context
}
export default useLibraryNavigation

View File

@@ -5,10 +5,8 @@ export default function TracksScreen({ route, navigation }: TracksProps): React.
return (
<Tracks
navigation={navigation}
tracks={route.params.tracks}
queue={route.params.queue}
fetchNextPage={route.params.fetchNextPage}
hasNextPage={route.params.hasNextPage}
tracksInfiniteQuery={route.params.tracksInfiniteQuery}
queue={'Library'}
/>
)
}

View File

@@ -37,11 +37,7 @@ export type BaseStackParamList = {
}
Tracks: {
tracks: BaseItemDto[] | undefined
queue: Queue
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
tracksInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
}

View File

@@ -21,6 +21,8 @@ import { isUndefined } from 'lodash'
import uuid from 'react-native-uuid'
import { convertRunTimeTicksToSeconds } from './runtimeticks'
import { DownloadQuality } from '../stores/settings/usage'
import MediaInfoQueryKey from '../api/queries/media/keys'
import { JellifyUser } from '../types/JellifyUser'
/**
* Gets quality-specific parameters for transcoding
@@ -83,11 +85,9 @@ export function mapDtoToTrack(
): JellifyTrack {
const downloads = downloadedTracks.filter((download) => download.item.Id === item.Id)
const mediaInfo = queryClient.getQueryData([
QueryKeys.MediaSources,
deviceProfile?.Name,
item.Id,
]) as PlaybackInfoResponse | undefined
const mediaInfo = queryClient.getQueryData(
MediaInfoQueryKey({ api, deviceProfile, itemId: item.Id }),
) as PlaybackInfoResponse | undefined
let trackMediaInfo: TrackMediaInfo
@@ -188,11 +188,9 @@ function buildAudioApiUrl(
console.debug(
`Mapping BaseItemDTO to Track object with streaming quality: ${deviceProfile?.Name}`,
)
const mediaInfo = queryClient.getQueryData([
QueryKeys.MediaSources,
deviceProfile?.Name,
item.Id,
]) as PlaybackInfoResponse | undefined
const mediaInfo = queryClient.getQueryData(
MediaInfoQueryKey({ api, deviceProfile, itemId: item.Id }),
) as PlaybackInfoResponse | undefined
let urlParams: Record<string, string> = {}
let container: string = 'mp3'

View File

@@ -1,3 +1,4 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
import { InfiniteData } from '@tanstack/react-query'
import { isString } from 'lodash'
@@ -23,7 +24,7 @@ export default function flattenInfiniteQueryPages(
const flashListItems: (string | number | BaseItemDto)[] = []
flattenedItemPages.forEach((item: BaseItemDto) => {
const rawLetter = isString(item.SortName) ? item.SortName.charAt(0).toUpperCase() : '#'
const rawLetter = extractFirstLetter(item)
/**
* An alpha character or a hash if the artist's name doesn't start with a letter
@@ -42,3 +43,13 @@ export default function flattenInfiniteQueryPages(
return flashListItems
}
function extractFirstLetter({ Type, SortName, Name }: BaseItemDto): string {
let letter = '#'
if (Type === BaseItemKind.Audio)
letter = isString(Name) ? Name.trim().charAt(0).toUpperCase() : '#'
else letter = isString(SortName) ? SortName.charAt(0).toUpperCase() : '#'
return letter
}

View File

@@ -8421,7 +8421,7 @@ react-dom@^19.1.0:
dependencies:
scheduler "^0.26.0"
react-freeze@^1.0.0, react-freeze@^1.0.3, react-freeze@^1.0.4:
react-freeze@^1.0.0, react-freeze@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==
@@ -8571,10 +8571,10 @@ react-native-safe-area-context@^5.6.1:
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz#cb4d249ef1a6f7e8fd0cfdfa9764838dffda26b6"
integrity sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==
react-native-screens@4.15.4:
version "4.15.4"
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.15.4.tgz#834541023fd26589d2c81fcbe35576553caee840"
integrity sha512-aKHPDScUbpQiZEG9eZssHdG5jEQs4yiJ8eMx6g81Ex/xU7DZkv3911enzdCb+v4eJE79X8waizY0ZhauZJQmrw==
react-native-screens@4.16.0:
version "4.16.0"
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.16.0.tgz#efa42e77a092aa0b5277c9ae41391ea0240e0870"
integrity sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==
dependencies:
react-freeze "^1.0.0"
react-native-is-edge-to-edge "^1.2.1"
@@ -9830,11 +9830,6 @@ 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"