Multiple Artist Support in Player (#441)

* Add support for navigating to multiple artists in the player

fix some display issues in the artists screen, namely displaying an indicator when loading and hiding sections when we don't have artists that start with a given letter
This commit is contained in:
Violet Caulfield
2025-07-15 07:23:27 -05:00
committed by GitHub
parent c02b587bdd
commit fd97126f2f
12 changed files with 996 additions and 911 deletions
-4
View File
@@ -34,10 +34,6 @@ appId: com.jellify
- tapOn:
id: "play-button-test-id"
# check if the pause button is visible
- assertVisible:
id: "pause-button-test-id"
# check if the queue button is visible
- assertVisible:
id: 'queue-button-test-id'
+2 -2
View File
@@ -44,7 +44,7 @@
"@react-navigation/native-stack": "^7.3.21",
"@sentry/react-native": "^6.17.0",
"@shopify/flash-list": "^2.0.0-rc.11",
"@tamagui/config": "^1.132.1",
"@tamagui/config": "^1.132.6",
"@tanstack/query-sync-storage-persister": "^5.83.0",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-persist-client": "^5.83.0",
@@ -87,7 +87,7 @@
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"ruby": "^0.6.1",
"tamagui": "^1.132.1"
"tamagui": "^1.132.6"
},
"devDependencies": {
"@babel/core": "^7.28.0",
+33 -26
View File
@@ -11,6 +11,7 @@ import { FlashList } from '@shopify/flash-list'
import { useLibraryContext } from '../../providers/Library'
import { sleepify } from '../../utils/sleep'
import { AZScroller } from '../Global/components/alphabetical-selector'
import { useMutation } from '@tanstack/react-query'
export default function Artists({
artists,
@@ -50,18 +51,19 @@ export default function Artists({
!artistPageParams.current.includes(letter)) &&
hasNextPage
)
await sleepify(250)
sectionListRef.current?.scrollToIndex({
index:
(artistsRef.current?.indexOf(letter) ?? 0) > -1
? artistsRef.current!.indexOf(letter)
: 0,
viewPosition: 0.1,
animated: true,
})
}
const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } = useMutation({
mutationFn: (letter: string) => alphabeticalSelectorCallback(letter),
onSuccess: (data, letter) => {
sectionListRef.current?.scrollToIndex({
index: artistsRef.current!.indexOf(letter),
viewPosition: 0.1,
animated: true,
})
},
})
useEffect(() => {
artistsRef.current = artists ?? []
}, [artists])
@@ -89,21 +91,28 @@ export default function Artists({
ItemSeparatorComponent={() => <Separator />}
estimatedItemSize={itemHeight}
data={artists}
refreshControl={<RefreshControl refreshing={isPending} />}
refreshControl={
<RefreshControl refreshing={isPending || isAlphabetSelectorPending} />
}
renderItem={({ index, item: artist }) =>
typeof artist === 'string' ? (
<XStack
padding={'$2'}
backgroundColor={'$background'}
borderRadius={'$5'}
borderWidth={'$1'}
borderColor={'$primary'}
margin={'$2'}
>
<Text bold color={'$primary'}>
{artist.toUpperCase()}
</Text>
</XStack>
// Don't render the letter if we don't have any artists that start with it
// If the index is the last index, or the next index is not an object, then don't render the letter
index - 1 === artists!.length ||
typeof artists![index + 1] !== 'object' ? null : (
<XStack
padding={'$2'}
backgroundColor={'$background'}
borderRadius={'$5'}
borderWidth={'$1'}
borderColor={'$primary'}
margin={'$2'}
>
<Text bold color={'$primary'}>
{artist.toUpperCase()}
</Text>
</XStack>
)
) : typeof artist === 'number' ? null : typeof artist === 'object' ? (
<MemoizedItem
item={artist}
@@ -137,9 +146,7 @@ export default function Artists({
removeClippedSubviews={false}
/>
{showAlphabeticalSelector && (
<AZScroller onLetterSelect={alphabeticalSelectorCallback} />
)}
{showAlphabeticalSelector && <AZScroller onLetterSelect={alphabetSelectorMutate} />}
</XStack>
)
}
@@ -0,0 +1,38 @@
import { ScrollView, View } from 'tamagui'
import { MultipleArtistsProps } from '../../types'
import ItemRow from '../../Global/components/item-row'
import { useEffect } from 'react'
export default function MultipleArtists({
navigation,
route,
}: MultipleArtistsProps): React.JSX.Element {
return (
<ScrollView>
{route.params.artists.map((artist) => {
return (
<ItemRow
circular
key={artist.Id}
item={artist}
queueName={''}
navigation={navigation}
onPress={() => {
navigation.goBack()
navigation.goBack()
navigation.navigate('Tabs', {
screen: 'Library',
params: {
screen: 'Artist',
params: {
artist: artist,
},
},
})
}}
/>
)
})}
</ScrollView>
)
}
+10 -1
View File
@@ -28,10 +28,14 @@ export default function ItemRow({
item,
queueName,
navigation,
onPress,
circular,
}: {
item: BaseItemDto
queueName: string
navigation: NativeStackNavigationProp<StackParamList>
onPress?: () => void
circular?: boolean
}): React.JSX.Element {
const { useStartPlayback } = usePlayerContext()
const { useLoadNewQueue } = useQueueContext()
@@ -77,6 +81,11 @@ export default function ItemRow({
})
}}
onPress={() => {
if (onPress) {
onPress()
return
}
switch (item.Type) {
case 'MusicArtist': {
navigation.navigate('Artist', {
@@ -101,7 +110,7 @@ export default function ItemRow({
item={item}
height={'$12'}
width={'$12'}
circular={item.Type === 'MusicArtist'}
circular={item.Type === 'MusicArtist' || circular}
/>
</YStack>
+14 -8
View File
@@ -62,16 +62,22 @@ export default function SongInfo({
color={'$color'}
onPress={() => {
if (nowPlaying!.item.ArtistItems) {
navigation.goBack() // Dismiss player modal
navigation.navigate('Tabs', {
screen: 'Library',
params: {
screen: 'Artist',
if (nowPlaying!.item.ArtistItems!.length > 1) {
navigation.navigate('MultipleArtists', {
artists: nowPlaying!.item.ArtistItems!,
})
} else {
navigation.goBack() // Dismiss player modal
navigation.navigate('Tabs', {
screen: 'Library',
params: {
artist: nowPlaying!.item.ArtistItems![0],
screen: 'Artist',
params: {
artist: nowPlaying!.item.ArtistItems![0],
},
},
},
})
})
}
}
}}
>
+5
View File
@@ -87,6 +87,10 @@ export type StackParamList = {
Player: undefined
Queue: undefined
MultipleArtists: {
artists: BaseItemDto[]
}
Artist: {
artist: BaseItemDto
}
@@ -126,6 +130,7 @@ export type LibrarySelectionProps = NativeStackScreenProps<StackParamList, 'Libr
export type TabProps = NativeStackScreenProps<StackParamList, 'Tabs'>
export type PlayerProps = NativeStackScreenProps<StackParamList, 'Player'>
export type MultipleArtistsProps = NativeStackScreenProps<StackParamList, 'MultipleArtists'>
export type ProvidedHomeProps = NativeStackScreenProps<StackParamList, 'HomeScreen'>
export type AddPlaylistProps = NativeStackScreenProps<StackParamList, 'AddPlaylist'>
+6
View File
@@ -0,0 +1,6 @@
import { MultipleArtistsProps } from '../../components/types'
import MultipleArtists from '../../components/Context/components/multiple-artists'
export default function MultipleArtistsSheet(props: MultipleArtistsProps): React.JSX.Element {
return <MultipleArtists {...props} />
}
+16
View File
@@ -4,6 +4,7 @@ import PlayerScreen from '../../components/Player'
import Queue from '../../components/Player/queue'
import DetailsScreen from '../Detail'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import MultipleArtistsSheet from '../Context/multiple-artists'
export const PlayerStack = createNativeStackNavigator<StackParamList>()
@@ -34,6 +35,21 @@ export default function Player(): React.JSX.Element {
headerTitle: '',
}}
/>
<PlayerStack.Group
screenOptions={{
presentation: 'formSheet',
sheetAllowedDetents: [0.2],
}}
>
<PlayerStack.Screen
name='MultipleArtists'
component={MultipleArtistsSheet}
options={{
headerShown: false,
}}
/>
</PlayerStack.Group>
</PlayerStack.Navigator>
)
}
+1
View File
@@ -37,6 +37,7 @@ export default function Root(): React.JSX.Element {
options={{
headerShown: false,
presentation: 'modal',
sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [1.0],
}}
/>
<RootStack.Screen
+1
View File
@@ -86,6 +86,7 @@ export function Tabs({
name='Library'
component={LibraryStack}
options={{
lazy: false,
headerShown: false,
tabBarIcon: ({ color, size }) => (
<MaterialCommunityIcons
+870 -870
View File
File diff suppressed because it is too large Load Diff