mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2026-01-06 22:00:59 -06:00
Add Suggested Artists to Discover Tab (#468)
* Fix issue where frequent artists werent properly sorted * adding suggested artists to discover page adding maestro test coverage to discover tab * Add Suggested artists
This commit is contained in:
@@ -2,6 +2,6 @@ appId: com.jellify
|
||||
---
|
||||
- clearState # clears the state of the current app
|
||||
- launchApp
|
||||
- runFlow: ../tests/login.yaml
|
||||
- runFlow: ../tests/musicplayer.yaml
|
||||
- runFlow: ../tests/search.yaml
|
||||
- runFlow: ../tests/1-login.yaml
|
||||
- runFlow: ../tests/2-library.yaml
|
||||
- runFlow: ../tests/3-musicplayer.yaml
|
||||
@@ -1,4 +1,5 @@
|
||||
appId: com.jellify
|
||||
---
|
||||
- runFlow: ../tests/library.yaml
|
||||
- runFlow: ../tests/settings.yaml
|
||||
- runFlow: ../tests/4-search.yaml
|
||||
- runFlow: ../tests/5-discover.yaml
|
||||
- runFlow: ../tests/6-settings.yaml
|
||||
|
||||
20
maestro/tests/5-discover.yaml
Normal file
20
maestro/tests/5-discover.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
appId: com.jellify
|
||||
---
|
||||
# Wait for app to be ready, then navigate to Settings tab
|
||||
- assertVisible:
|
||||
id: "discover-tab-button"
|
||||
|
||||
# Navigate to Library tab using text
|
||||
- tapOn:
|
||||
id: "discover-tab-button"
|
||||
|
||||
# Verify we're on the library page
|
||||
- assertVisible:
|
||||
id: "discover-recently-added"
|
||||
|
||||
- assertVisible:
|
||||
id: "discover-public-playlists"
|
||||
|
||||
- assertVisible:
|
||||
id: "discover-suggested-artists"
|
||||
|
||||
@@ -139,4 +139,4 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function fetchArtists(
|
||||
startIndex: page * QueryConfig.limits.library,
|
||||
limit: QueryConfig.limits.library,
|
||||
isFavorite: isFavorite,
|
||||
fields: [ItemFields.SortName],
|
||||
fields: [ItemFields.SortName, ItemFields.ChildCount],
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug('Artists Response received')
|
||||
|
||||
@@ -93,10 +93,12 @@ export function fetchFrequentlyPlayedArtists(
|
||||
)
|
||||
})
|
||||
.then((artistsWithPlayCounts) => {
|
||||
console.debug('Fetching artists', artistsWithPlayCounts)
|
||||
const artists = artistsWithPlayCounts.map((artist) => {
|
||||
return fetchItem(api, artist.artistId)
|
||||
})
|
||||
console.debug('Fetching artists')
|
||||
const artists = artistsWithPlayCounts
|
||||
.sort((a, b) => b.playCount - a.playCount)
|
||||
.map((artist) => {
|
||||
return fetchItem(api, artist.artistId)
|
||||
})
|
||||
|
||||
return Promise.all(artists)
|
||||
})
|
||||
|
||||
@@ -42,3 +42,35 @@ export async function fetchSearchSuggestions(
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchArtistSuggestions(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
libraryId: string | undefined,
|
||||
page: number,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
if (isUndefined(user)) return reject('User has not been set')
|
||||
if (isUndefined(libraryId)) return reject('Library has not been set')
|
||||
|
||||
getItemsApi(api)
|
||||
.getItems({
|
||||
parentId: libraryId,
|
||||
userId: user.id,
|
||||
recursive: true,
|
||||
limit: 50,
|
||||
startIndex: page * 50,
|
||||
includeItemTypes: [BaseItemKind.MusicArtist],
|
||||
fields: ['ChildCount'],
|
||||
sortBy: ['Random'],
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.Items) resolve(data.Items)
|
||||
else resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import Artists from './component'
|
||||
import { ArtistsProps, StackParamList } from '../types'
|
||||
import { ArtistsProps } from '../types'
|
||||
|
||||
export default function ArtistsScreen({
|
||||
navigation,
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import React from 'react'
|
||||
import { getToken, ScrollView } from 'tamagui'
|
||||
import { getToken, ScrollView, Separator, View } from 'tamagui'
|
||||
import RecentlyAdded from './helpers/just-added'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import { useDiscoverContext } from '../../providers/Discover'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import PublicPlaylists from './helpers/public-playlists'
|
||||
import SuggestedArtists from './helpers/suggested-artists'
|
||||
|
||||
export default function Index({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { refreshing, refresh, recentlyAdded, publicPlaylists } = useDiscoverContext()
|
||||
const { refreshing, refresh, recentlyAdded, publicPlaylists, suggestedArtistsInfiniteQuery } =
|
||||
useDiscoverContext()
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -26,8 +28,26 @@ export default function Index({
|
||||
paddingBottom={'$15'}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}
|
||||
>
|
||||
{recentlyAdded && <RecentlyAdded navigation={navigation} />}
|
||||
{publicPlaylists && <PublicPlaylists navigation={navigation} />}
|
||||
{recentlyAdded && (
|
||||
<View testID='discover-recently-added'>
|
||||
<RecentlyAdded navigation={navigation} />
|
||||
<Separator marginVertical={'$2'} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{publicPlaylists && (
|
||||
<View testID='discover-public-playlists'>
|
||||
<PublicPlaylists navigation={navigation} />
|
||||
<Separator marginVertical={'$2'} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{suggestedArtistsInfiniteQuery.data && (
|
||||
<View testID='discover-suggested-artists'>
|
||||
<SuggestedArtists navigation={navigation} />
|
||||
<Separator marginVertical={'$2'} />
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
47
src/components/Discover/helpers/suggested-artists.tsx
Normal file
47
src/components/Discover/helpers/suggested-artists.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { View, XStack } from 'tamagui'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import HorizontalCardList from '../../Global/components/horizontal-list'
|
||||
import { ItemCard } from '../../Global/components/item-card'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useDiscoverContext } from '../../../providers/Discover'
|
||||
import { H4 } from '../../Global/helpers/text'
|
||||
import { StackParamList } from '../../types'
|
||||
|
||||
export default function SuggestedArtists({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { suggestedArtistsInfiniteQuery } = useDiscoverContext()
|
||||
return (
|
||||
<View>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() => {
|
||||
navigation.navigate('SuggestedArtists', {
|
||||
artistsInfiniteQuery: suggestedArtistsInfiniteQuery,
|
||||
navigation: navigation,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<H4>Suggested Artists</H4>
|
||||
<Icon name='arrow-right' />
|
||||
</XStack>
|
||||
<HorizontalCardList
|
||||
data={suggestedArtistsInfiniteQuery.data?.slice(0, 10) ?? []}
|
||||
renderItem={({ item }) => (
|
||||
<ItemCard
|
||||
caption={item.Name}
|
||||
size={'$11'}
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist', {
|
||||
artist: item,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -112,6 +112,11 @@ export default function ItemRow({
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.Name ?? ''}
|
||||
</Text>
|
||||
{item.Type === 'MusicArtist' && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{`${item.ChildCount ?? 0} ${item.ChildCount === 1 ? 'Album' : 'Albums'}`}
|
||||
</Text>
|
||||
)}
|
||||
{(item.Type === 'Audio' || item.Type === 'MusicAlbum') && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.AlbumArtist ?? 'Untitled Artist'}
|
||||
|
||||
11
src/components/types.d.ts
vendored
11
src/components/types.d.ts
vendored
@@ -70,6 +70,10 @@ export type StackParamList = {
|
||||
isFetchingNextPage: boolean
|
||||
refetch: () => void
|
||||
}
|
||||
SuggestedArtists: {
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}
|
||||
|
||||
LibraryScreen: undefined
|
||||
Library: undefined
|
||||
@@ -153,6 +157,8 @@ export type UserPlaylistsProps = NativeStackScreenProps<StackParamList, 'UserPla
|
||||
export type DiscoverProps = NativeStackScreenProps<StackParamList, 'Discover'>
|
||||
export type RecentlyAddedProps = NativeStackScreenProps<StackParamList, 'RecentlyAdded'>
|
||||
export type PublicPlaylistsProps = NativeStackScreenProps<StackParamList, 'PublicPlaylists'>
|
||||
export type SuggestedArtistsProps = NativeStackScreenProps<StackParamList, 'SuggestedArtists'>
|
||||
|
||||
export type HomeArtistProps = NativeStackScreenProps<StackParamList, 'Artist'>
|
||||
export type ArtistAlbumsProps = NativeStackScreenProps<StackParamList, 'ArtistAlbums'>
|
||||
export type ArtistEpsProps = NativeStackScreenProps<StackParamList, 'ArtistEps'>
|
||||
@@ -169,7 +175,10 @@ export type TracksProps = NativeStackScreenProps<StackParamList, 'Tracks'>
|
||||
|
||||
export type ArtistsProps = {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<
|
||||
BaseItemDto[] | (string | number | BaseItemDto)[],
|
||||
Error
|
||||
>
|
||||
showAlphabeticalSelector: boolean
|
||||
artistPageParams?: RefObject<Set<string>>
|
||||
}
|
||||
|
||||
@@ -111,4 +111,9 @@ export enum QueryKeys {
|
||||
* Query representing the fetching of albums in an infinite query
|
||||
*/
|
||||
InfiniteAlbums = 'InfiniteAlbums',
|
||||
|
||||
/**
|
||||
* Query representing the fetching of suggested artists in an infinite query
|
||||
*/
|
||||
InfiniteSuggestedArtists = 'InfiniteSuggestedArtists',
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { InfiniteData, useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
InfiniteData,
|
||||
InfiniteQueryObserverResult,
|
||||
useInfiniteQuery,
|
||||
UseInfiniteQueryResult,
|
||||
} from '@tanstack/react-query'
|
||||
import { fetchRecentlyAdded, 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 { fetchArtistSuggestions } from '../../api/queries/suggestions'
|
||||
|
||||
interface DiscoverContext {
|
||||
refreshing: boolean
|
||||
refresh: () => void
|
||||
@@ -24,6 +31,7 @@ interface DiscoverContext {
|
||||
isFetchingNextRecentlyPlayed: boolean
|
||||
isFetchingNextPublicPlaylists: boolean
|
||||
refetchPublicPlaylists: () => void
|
||||
suggestedArtistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
|
||||
}
|
||||
|
||||
const DiscoverContextInitializer = () => {
|
||||
@@ -41,7 +49,8 @@ const DiscoverContextInitializer = () => {
|
||||
queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, pages) => (lastPage.length > 0 ? pages.length + 1 : undefined),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
@@ -56,7 +65,8 @@ const DiscoverContextInitializer = () => {
|
||||
queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId],
|
||||
queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, pages) => (lastPage.length > 0 ? pages.length + 1 : undefined),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
@@ -70,10 +80,22 @@ const DiscoverContextInitializer = () => {
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayed, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyPlayed(api, user, library, pageParam),
|
||||
getNextPageParam: (lastPage, pages) => (lastPage.length > 0 ? pages.length + 1 : undefined),
|
||||
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 }) =>
|
||||
fetchArtistSuggestions(api, user, library?.musicLibraryId, pageParam),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
maxPages: 2,
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
setRefreshing(true)
|
||||
|
||||
@@ -81,6 +103,7 @@ const DiscoverContextInitializer = () => {
|
||||
refetchRecentlyAdded(),
|
||||
refetchRecentlyPlayed(),
|
||||
refetchPublicPlaylists(),
|
||||
suggestedArtistsInfiniteQuery.refetch(),
|
||||
])
|
||||
setRefreshing(false)
|
||||
}
|
||||
@@ -104,6 +127,7 @@ const DiscoverContextInitializer = () => {
|
||||
isFetchingNextRecentlyPlayed,
|
||||
isFetchingNextPublicPlaylists,
|
||||
refetchPublicPlaylists,
|
||||
suggestedArtistsInfiniteQuery,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +150,45 @@ const DiscoverContext = createContext<DiscoverContext>({
|
||||
isFetchingNextRecentlyPlayed: false,
|
||||
isFetchingNextPublicPlaylists: false,
|
||||
refetchPublicPlaylists: () => {},
|
||||
suggestedArtistsInfiniteQuery: {
|
||||
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([]),
|
||||
},
|
||||
})
|
||||
|
||||
export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
|
||||
|
||||
12
src/screens/Discover/artists.tsx
Normal file
12
src/screens/Discover/artists.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Artists from '../../components/Artists/component'
|
||||
import { SuggestedArtistsProps } from '../../components/types'
|
||||
|
||||
export default function SuggestedArtists({ navigation, route }: SuggestedArtistsProps) {
|
||||
return (
|
||||
<Artists
|
||||
navigation={navigation}
|
||||
artistsInfiniteQuery={route.params.artistsInfiniteQuery}
|
||||
showAlphabeticalSelector={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { useTheme } from 'tamagui'
|
||||
import RecentlyAdded from './albums'
|
||||
import PublicPlaylists from './playlists'
|
||||
import { PlaylistScreen } from '../Playlist'
|
||||
import SuggestedArtists from './artists'
|
||||
|
||||
export const DiscoverStack = createNativeStackNavigator<StackParamList>()
|
||||
|
||||
@@ -82,6 +83,14 @@ export function Discover(): React.JSX.Element {
|
||||
}}
|
||||
/>
|
||||
|
||||
<DiscoverStack.Screen
|
||||
name='SuggestedArtists'
|
||||
component={SuggestedArtists}
|
||||
options={{
|
||||
title: 'Suggested Artists',
|
||||
}}
|
||||
/>
|
||||
|
||||
<DiscoverStack.Screen
|
||||
name='InstantMix'
|
||||
component={InstantMix}
|
||||
|
||||
Reference in New Issue
Block a user