mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-16 18:55:44 -06:00
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:
3
index.js
3
index.js
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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: () => [
|
||||
|
||||
40
src/api/queries/frequents/index.ts
Normal file
40
src/api/queries/frequents/index.ts
Normal 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),
|
||||
})
|
||||
}
|
||||
23
src/api/queries/frequents/keys.ts
Normal file
23
src/api/queries/frequents/keys.ts
Normal 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)
|
||||
@@ -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,
|
||||
24
src/api/queries/home/index.ts
Normal file
24
src/api/queries/home/index.ts
Normal 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
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
16
src/api/queries/media/keys.ts
Normal file
16
src/api/queries/media/keys.ts
Normal 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
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
19
src/api/queries/playlist/index.ts
Normal file
19
src/api/queries/playlist/index.ts
Normal 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
|
||||
},
|
||||
})
|
||||
}
|
||||
16
src/api/queries/playlist/keys.ts
Normal file
16
src/api/queries/playlist/keys.ts
Normal 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,
|
||||
]
|
||||
@@ -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,
|
||||
@@ -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 = {
|
||||
|
||||
40
src/api/queries/recents/index.ts
Normal file
40
src/api/queries/recents/index.ts
Normal 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),
|
||||
})
|
||||
}
|
||||
24
src/api/queries/recents/keys.ts
Normal file
24
src/api/queries/recents/keys.ts
Normal 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)
|
||||
@@ -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([])
|
||||
}
|
||||
90
src/api/queries/track/index.ts
Normal file
90
src/api/queries/track/index.ts
Normal 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
|
||||
}
|
||||
20
src/api/queries/track/keys.ts
Normal file
20
src/api/queries/track/keys.ts
Normal 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,
|
||||
]
|
||||
@@ -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([])
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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!],
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function Index(): React.JSX.Element {
|
||||
{suggestedArtistsInfiniteQuery.data && (
|
||||
<View testID='discover-suggested-artists'>
|
||||
<SuggestedArtists />
|
||||
<Separator marginVertical={'$2'} />
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>()
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -103,7 +103,6 @@ export default function Playlist({
|
||||
marginHorizontal: 2,
|
||||
}}
|
||||
onScroll={scrollOffsetHandler}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
)
|
||||
|
||||
10
src/screens/Home/types.d.ts
vendored
10
src/screens/Home/types.d.ts
vendored
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
6
src/screens/types.d.ts
vendored
6
src/screens/types.d.ts
vendored
@@ -37,11 +37,7 @@ export type BaseStackParamList = {
|
||||
}
|
||||
|
||||
Tracks: {
|
||||
tracks: BaseItemDto[] | undefined
|
||||
queue: Queue
|
||||
fetchNextPage: () => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
tracksInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
15
yarn.lock
15
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user