Nitro Fetch Axios Adapter, Artist Page Additions (#866)

* artist page tweaks, incorporate nitro fetch axios adapter for networking requests

* fix pr otas

* queue and playlist fixes

* media info query staletime adjustments
This commit is contained in:
Violet Caulfield
2025-12-23 10:24:56 -06:00
committed by GitHub
parent 22794ff528
commit 328007e6ce
30 changed files with 305 additions and 400 deletions

View File

@@ -7,7 +7,7 @@ import { fetchMediaInfo } from './utils'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import MediaInfoQueryKey from './keys'
import { useApi } from '../../../stores'
import { ONE_DAY } from '../../../constants/query-client'
import { ONE_DAY, ONE_HOUR } from '../../../constants/query-client'
/**
* A React hook that will retrieve the latest media info
@@ -32,8 +32,8 @@ const useStreamedMediaInfo = (itemId: string | null | undefined) => {
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
enabled: Boolean(api && deviceProfile && itemId),
staleTime: ONE_DAY, // Only refetch when the user's device profile changes
gcTime: ONE_DAY,
staleTime: Infinity, // Only refetch when the user's device profile changes
gcTime: Infinity,
})
}
@@ -62,7 +62,7 @@ export const useDownloadedMediaInfo = (itemId: string | null | undefined) => {
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
enabled: Boolean(api && deviceProfile && itemId),
staleTime: ONE_DAY, // Only refetch when the user's device profile changes
gcTime: ONE_DAY,
staleTime: ONE_HOUR * 6, // Only refetch when the user's device profile changes
gcTime: ONE_HOUR * 6,
})
}

View File

@@ -6,7 +6,7 @@ import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { FlashList } from '@shopify/flash-list'
import { YStack, H5 } from 'tamagui'
import { ItemCard } from '../Global/components/item-card'
import ItemCard from '../Global/components/item-card'
export default function AlbumTrackListFooter({ album }: { album: BaseItemDto }): React.JSX.Element {
const navigation =

View File

@@ -18,7 +18,7 @@ import useAddToPendingDownloads, { useIsDownloading } from '../../stores/network
import { useIsDownloaded } from '../../api/queries/download'
import AlbumTrackListFooter from './footer'
import AlbumTrackListHeader from './header'
import Animated, { FadeInUp, FadeOutDown, LinearTransition } from 'react-native-reanimated'
import Animated, { FadeIn, FadeOutDown, LinearTransition } from 'react-native-reanimated'
import { useStorageContext } from '../../providers/Storage'
/**
@@ -69,7 +69,7 @@ export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
{albumTrackList &&
(isDownloaded ? (
<Animated.View
entering={FadeInUp.springify()}
entering={FadeIn.springify()}
exiting={FadeOutDown.springify()}
layout={LinearTransition.springify()}
>
@@ -83,7 +83,7 @@ export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
<Spinner justifyContent='center' color={'$neutral'} />
) : (
<Animated.View
entering={FadeInUp.springify()}
entering={FadeIn.springify()}
exiting={FadeOutDown.springify()}
layout={LinearTransition.springify()}
>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'
import React from 'react'
import { useArtistContext } from '../../providers/Artist'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '@/src/screens/types'
@@ -16,43 +16,42 @@ export default function ArtistOverviewTab({
}): React.JSX.Element {
const { featuredOn, artist, albums } = useArtistContext()
const sections: SectionListData<BaseItemDto>[] = useMemo(() => {
return [
{
title: 'Albums',
data: albums?.filter(({ ChildCount }) => (ChildCount ?? 0) > 6) ?? [],
},
{
title: 'EPs',
data:
albums?.filter(
({ ChildCount }) => (ChildCount ?? 0) <= 6 && (ChildCount ?? 0) >= 3,
) ?? [],
},
{
title: 'Singles',
data: albums?.filter(({ ChildCount }) => (ChildCount ?? 0) === 1) ?? [],
},
{
title: '',
data: albums?.filter(({ ChildCount }) => typeof ChildCount !== 'number') ?? [],
},
{
title: 'Featured On',
data: featuredOn ?? [],
},
]
}, [artist, albums?.map(({ Id }) => Id)])
const sections: SectionListData<BaseItemDto>[] = [
{
title: 'Albums',
data: albums?.filter(({ ChildCount }) => (ChildCount ?? 0) > 6) ?? [],
},
{
title: 'EPs',
data:
albums?.filter(
({ ChildCount }) => (ChildCount ?? 0) <= 6 && (ChildCount ?? 0) >= 3,
) ?? [],
},
{
title: 'Singles',
data: albums?.filter(({ ChildCount }) => (ChildCount ?? 0) === 1) ?? [],
},
{
title: '',
data: albums?.filter(({ ChildCount }) => typeof ChildCount !== 'number') ?? [],
},
{
title: 'Featured On',
data: featuredOn ?? [],
},
]
const renderSectionHeader = useCallback(
({ section }: { section: SectionListData<BaseItemDto, DefaultSectionT> }) =>
section.data.length > 0 ? (
<Text padding={'$3'} fontSize={'$6'} bold backgroundColor={'$background'}>
{section.title}
</Text>
) : null,
[],
)
const renderSectionHeader = ({
section,
}: {
section: SectionListData<BaseItemDto, DefaultSectionT>
}) =>
section.data.length > 0 ? (
<Text padding={'$2'} fontSize={'$6'} bold backgroundColor={'$background'}>
{section.title}
</Text>
) : null
return (
<SectionList

View File

@@ -1,94 +0,0 @@
import { MaterialTopTabBarProps } from '@react-navigation/material-top-tabs'
import React from 'react'
import { Square, XStack, YStack } from 'tamagui'
import Icon from '../Global/components/icon'
import { Text } from '../Global/helpers/text'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import { ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client'
import { MaterialTopTabBar } from '@react-navigation/material-top-tabs'
interface ArtistTabBarProps extends MaterialTopTabBarProps {
isFavorites: boolean
setIsFavorites: (isFavorites: boolean) => void
sortBy: ItemSortBy
setSortBy: (sortBy: ItemSortBy) => void
sortOrder: SortOrder
setSortOrder: (sortOrder: SortOrder) => void
}
export default function ArtistTabBar({
isFavorites,
setIsFavorites,
sortBy,
setSortBy,
sortOrder,
setSortOrder,
...props
}: ArtistTabBarProps) {
const trigger = useHapticFeedback()
const insets = useSafeAreaInsets()
return (
<YStack>
<MaterialTopTabBar {...props} />
{props.state.routes[props.state.index].name === 'Tracks' && (
<XStack
borderColor={'$borderColor'}
alignContent={'flex-start'}
justifyContent='flex-start'
paddingHorizontal={'$1'}
paddingVertical={'$2'}
gap={'$2'}
maxWidth={'80%'}
>
<XStack
onPress={() => {
trigger('impactLight')
setIsFavorites(!isFavorites)
}}
alignItems={'center'}
justifyContent={'center'}
>
<Icon
name={isFavorites ? 'heart' : 'heart-outline'}
color={isFavorites ? '$primary' : '$borderColor'}
/>
<Text color={isFavorites ? '$primary' : '$borderColor'}>
{isFavorites ? 'Favorites' : 'All'}
</Text>
</XStack>
<XStack
onPress={() => {
trigger('impactLight')
if (sortBy === ItemSortBy.DateCreated) {
setSortBy(ItemSortBy.SortName)
setSortOrder(SortOrder.Ascending)
} else {
setSortBy(ItemSortBy.DateCreated)
setSortOrder(SortOrder.Descending)
}
}}
alignItems={'center'}
justifyContent={'center'}
>
<Icon
name={
sortBy === ItemSortBy.DateCreated
? 'calendar'
: 'sort-alphabetical-ascending'
}
color={'$borderColor'}
/>{' '}
<Text color={'$borderColor'}>
{sortBy === ItemSortBy.DateCreated ? 'Date Added' : 'A-Z'}
</Text>
</XStack>
</XStack>
)}
</YStack>
)
}

View File

@@ -1,5 +1,5 @@
import { ImageType } from '@jellyfin/sdk/lib/generated-client'
import { XStack, YStack } from 'tamagui'
import { Text, XStack, YStack } from 'tamagui'
import ItemImage from '../Global/components/image'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { H5 } from '../Global/helpers/text'
@@ -16,6 +16,8 @@ import { QueuingType } from '../../enums/queuing-type'
import { useNetworkStatus } from '../../stores/network'
import useStreamingDeviceProfile from '../../stores/device-profile'
import { useApi } from '../../stores'
import Icon from '../Global/components/icon'
import useTracks from '../../api/queries/track'
export default function ArtistHeader(): React.JSX.Element {
const { width } = useSafeAreaFrame()
@@ -62,6 +64,8 @@ export default function ArtistHeader(): React.JSX.Element {
}
}
const [trackPageParams, tracksInfiniteQuery] = useTracks(artist.Id)
return (
<YStack flex={1}>
<ItemImage
@@ -73,7 +77,7 @@ export default function ArtistHeader(): React.JSX.Element {
imageOptions={{ maxWidth: width * 2, maxHeight: 640 }}
/>
<YStack alignItems='center' paddingHorizontal={'$3'}>
<YStack paddingHorizontal={'$2'}>
<XStack alignItems='flex-end' justifyContent='flex-start' flex={1}>
<XStack alignItems='center' flex={1} justifyContent='space-between'>
<H5 flexGrow={1} fontWeight={'bold'}>
@@ -90,10 +94,31 @@ export default function ArtistHeader(): React.JSX.Element {
</XStack>
<XStack alignItems='center' justifyContent='flex-end' gap={'$3'} flex={1}>
{/* <Icon name='shuffle' onPress={() => playArtist(true)} /> */}
<IconButton circular name='play' onPress={playArtist} />
<Icon
small
color='$primary'
name='shuffle'
onPress={() => playArtist(true)}
/>
<IconButton circular name='play' onPress={() => playArtist(false)} />
</XStack>
</XStack>
<XStack
alignItems='center'
flex={1}
justifyContent='flex-start'
marginVertical={'$2'}
onPress={() =>
navigation.push('Tracks', {
tracksInfiniteQuery,
})
}
>
<Text fontWeight={'bold'} fontSize={'$4'}>{`View Tracks`}</Text>
<Icon name='chevron-right' small />
</XStack>
</YStack>
</YStack>
)

View File

@@ -1,71 +1,12 @@
import React, { useState } from 'react'
import React from 'react'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '@/src/screens/types'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import ArtistOverviewTab from './OverviewTab'
import ArtistTracksTab from './TracksTab'
import ArtistTabBar from './TabBar'
import { ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client'
import { getTokenValue, useTheme } from 'tamagui'
const Tab = createMaterialTopTabNavigator()
export default function ArtistNavigation({
navigation,
}: {
navigation: NativeStackNavigationProp<BaseStackParamList>
}): React.JSX.Element {
const [isFavorites, setIsFavorites] = useState(false)
const [sortBy, setSortBy] = useState<ItemSortBy>(ItemSortBy.SortName)
const [sortOrder, setSortOrder] = useState<SortOrder>(SortOrder.Ascending)
const theme = useTheme()
return (
<Tab.Navigator
tabBar={(props) => (
<ArtistTabBar
{...props}
isFavorites={isFavorites}
setIsFavorites={setIsFavorites}
sortBy={sortBy}
setSortBy={setSortBy}
sortOrder={sortOrder}
setSortOrder={setSortOrder}
/>
)}
screenOptions={{
swipeEnabled: false,
tabBarIndicatorStyle: {
borderColor: theme.background.val,
borderBottomWidth: getTokenValue('$2'),
},
tabBarActiveTintColor: theme.background.val,
tabBarInactiveTintColor: theme.background50.val,
tabBarStyle: {
backgroundColor: theme.primary.val,
},
tabBarLabelStyle: {
fontSize: 16,
fontFamily: 'Figtree-Bold',
},
tabBarPressOpacity: 0.5,
lazy: true, // Enable lazy loading to prevent all tabs from mounting simultaneously
}}
>
<Tab.Screen name='Overview'>
{() => <ArtistOverviewTab navigation={navigation} />}
</Tab.Screen>
<Tab.Screen name='Tracks'>
{() => (
<ArtistTracksTab
navigation={navigation}
isFavorites={isFavorites}
sortBy={sortBy}
sortOrder={sortOrder}
/>
)}
</Tab.Screen>
</Tab.Navigator>
)
return <ArtistOverviewTab navigation={navigation} />
}

View File

@@ -15,7 +15,7 @@ export default function SimilarArtists(): React.JSX.Element {
return (
<YStack flex={1}>
<Text
margin={'$3'}
margin={'$2'}
fontSize={'$6'}
bold
>{`Similar to ${artist.Name ?? 'Unknown Artist'}`}</Text>

View File

@@ -1,6 +1,6 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { ItemCard } from '../../../components/Global/components/item-card'
import ItemCard from '../../../components/Global/components/item-card'
import { H5, View, XStack } from 'tamagui'
import Icon from '../../Global/components/icon'
import { useNavigation } from '@react-navigation/native'

View File

@@ -2,7 +2,7 @@ import { H5, XStack } from 'tamagui'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Icon from '../../Global/components/icon'
import HorizontalCardList from '../../Global/components/horizontal-list'
import { ItemCard } from '../../Global/components/item-card'
import ItemCard from '../../Global/components/item-card'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { useNavigation } from '@react-navigation/native'
import DiscoverStackParamList from '../../../screens/Discover/types'

View File

@@ -1,7 +1,7 @@
import { H5, 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 ItemCard from '../../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useNavigation } from '@react-navigation/native'
import DiscoverStackParamList from '../../../screens/Discover/types'

View File

@@ -57,8 +57,6 @@ export default function Icon({
onPress={onPress}
onPressIn={onPressIn}
hitSlop={getTokenValue('$2.5')}
width={size + getToken('$0.5')}
height={size + getToken('$0.5')}
flex={flex}
>
<MaterialDesignIcon

View File

@@ -1,4 +1,4 @@
import React, { memo, useCallback, useEffect, useMemo } from 'react'
import React, { useEffect } from 'react'
import { CardProps as TamaguiCardProps } from 'tamagui'
import { Card as TamaguiCard, View, YStack } from 'tamagui'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
@@ -23,7 +23,7 @@ interface CardProps extends TamaguiCardProps {
*
* @param props
*/
function ItemCardComponent({
export default function ItemCard({
caption,
subCaption,
item,
@@ -41,22 +41,16 @@ function ItemCardComponent({
if (item.Type === 'Audio') warmContext(item)
}, [item.Id, item.Type, warmContext])
const hoverStyle = useMemo(() => (onPress ? { scale: 0.925 } : undefined), [onPress])
const hoverStyle = onPress ? { scale: 0.925 } : undefined
const pressStyle = useMemo(() => (onPress ? { scale: 0.875 } : undefined), [onPress])
const pressStyle = onPress ? { scale: 0.875 } : undefined
const handlePressIn = useCallback(
() => (item.Type !== 'Audio' ? warmContext(item) : undefined),
[item.Id, warmContext],
)
const handlePressIn = () => (item.Type !== 'Audio' ? warmContext(item) : undefined)
const background = useMemo(
() => (
<TamaguiCard.Background>
<ItemImage item={item} circular={!squared} />
</TamaguiCard.Background>
),
[item.Id, squared],
const background = (
<TamaguiCard.Background>
<ItemImage item={item} circular={!squared} />
</TamaguiCard.Background>
)
return (
@@ -88,62 +82,41 @@ function ItemCardComponent({
)
}
const ItemCardComponentCaption = memo(
function ItemCardComponentCaption({
size,
captionAlign = 'center',
caption,
subCaption,
}: {
size: string | number
captionAlign: 'center' | 'left' | 'right'
caption?: string | null | undefined
subCaption?: string | null | undefined
}): React.JSX.Element | null {
if (!caption) return null
function ItemCardComponentCaption({
size,
captionAlign = 'center',
caption,
subCaption,
}: {
size: string | number
captionAlign: 'center' | 'left' | 'right'
caption?: string | null | undefined
subCaption?: string | null | undefined
}): React.JSX.Element | null {
if (!caption) return null
return (
<YStack maxWidth={size}>
return (
<YStack maxWidth={size}>
<Text
bold
lineBreakStrategyIOS='standard'
width={size}
numberOfLines={1}
textAlign={captionAlign}
>
{caption}
</Text>
{subCaption && (
<Text
bold
lineBreakStrategyIOS='standard'
width={size}
numberOfLines={1}
textAlign={captionAlign}
>
{caption}
{subCaption}
</Text>
{subCaption && (
<Text
lineBreakStrategyIOS='standard'
width={size}
numberOfLines={1}
textAlign={captionAlign}
>
{subCaption}
</Text>
)}
</YStack>
)
},
(prevProps, nextProps) =>
prevProps.size === nextProps.size &&
prevProps.captionAlign === nextProps.captionAlign &&
prevProps.caption === nextProps.caption &&
prevProps.subCaption === nextProps.subCaption,
)
export const ItemCard = React.memo(
ItemCardComponent,
(a, b) =>
a.item.Id === b.item.Id &&
a.item.Type === b.item.Type &&
a.caption === b.caption &&
a.subCaption === b.subCaption &&
a.squared === b.squared &&
a.size === b.size &&
a.testId === b.testId &&
!!a.onPress === !!b.onPress &&
a.captionAlign === b.captionAlign,
)
)}
</YStack>
)
}

View File

@@ -1,7 +1,7 @@
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React, { useCallback } from 'react'
import { ItemCard } from '../../../components/Global/components/item-card'
import ItemCard from '../../../components/Global/components/item-card'
import { H5, XStack } from 'tamagui'
import Icon from '../../Global/components/icon'
import { useDisplayContext } from '../../../providers/Display/display-provider'

View File

@@ -1,7 +1,7 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { H5, XStack } from 'tamagui'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { ItemCard } from '../../../components/Global/components/item-card'
import ItemCard from '../../../components/Global/components/item-card'
import { QueuingType } from '../../../enums/queuing-type'
import Icon from '../../Global/components/icon'
import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations'

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from 'react'
import { H5, View, XStack } from 'tamagui'
import { RootStackParamList } from '../../../screens/types'
import { ItemCard } from '../../Global/components/item-card'
import ItemCard from '../../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import Icon from '../../Global/components/icon'

View File

@@ -1,6 +1,6 @@
import React from 'react'
import { H5, XStack } from 'tamagui'
import { ItemCard } from '../../Global/components/item-card'
import ItemCard from '../../Global/components/item-card'
import { RootStackParamList } from '../../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { QueuingType } from '../../../enums/queuing-type'

View File

@@ -2,8 +2,8 @@ import Icon from '../Global/components/icon'
import Track from '../Global/components/track'
import { RootStackParamList } from '../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { ScrollView, XStack } from 'tamagui'
import { useLayoutEffect, useCallback, useState } from 'react'
import { ScrollView, Text, XStack } from 'tamagui'
import { useLayoutEffect, useState } from 'react'
import JellifyTrack from '../../types/JellifyTrack'
import {
useRemoveFromQueue,
@@ -13,8 +13,9 @@ import {
} from '../../providers/Player/hooks/mutations'
import { usePlayerQueueStore, useQueueRef } from '../../stores/player/queue'
import Sortable from 'react-native-sortables'
import { RenderItemInfo } from 'react-native-sortables/dist/typescript/types'
import { OrderChangeParams, RenderItemInfo } from 'react-native-sortables/dist/typescript/types'
import { useReducedHapticsSetting } from '../../stores/settings/app'
import uuid from 'react-native-uuid'
export default function Queue({
navigation,
@@ -35,51 +36,62 @@ export default function Queue({
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => {
return <Icon name='notification-clear-all' onPress={removeUpcomingTracks} />
return (
<XStack>
<Text>Clear Upcoming</Text>
<Icon
name='notification-clear-all'
onPress={async () => {
await removeUpcomingTracks()
setQueue(usePlayerQueueStore.getState().queue)
}}
/>
</XStack>
)
},
})
}, [navigation, removeUpcomingTracks])
const keyExtractor = useCallback((item: JellifyTrack) => `${item.item.Id}`, [])
const keyExtractor = (item: JellifyTrack) => item.item.Id ?? uuid.v4()
// Memoize renderItem function for better performance
const renderItem = useCallback(
({ item: queueItem, index }: RenderItemInfo<JellifyTrack>) => (
<XStack alignItems='center' key={`${index}-${queueItem.item.Id}`}>
<Sortable.Handle style={{ display: 'flex', flexShrink: 1 }}>
<Icon name='drag' />
</Sortable.Handle>
const renderItem = ({ item: queueItem, index }: RenderItemInfo<JellifyTrack>) => (
<XStack alignItems='center'>
<Sortable.Handle style={{ display: 'flex', flexShrink: 1 }}>
<Icon name='drag' />
</Sortable.Handle>
<Sortable.Touchable
onTap={() => skip(index)}
style={{
flexGrow: 1,
}}
>
<Track
queue={queueRef ?? 'Recently Played'}
track={queueItem.item}
index={index}
showArtwork
testID={`queue-item-${index}`}
isNested
editing
/>
</Sortable.Touchable>
<Sortable.Touchable
onTap={() => skip(index)}
style={{
flexGrow: 1,
}}
>
<Track
queue={queueRef ?? 'Recently Played'}
track={queueItem.item}
index={index}
showArtwork
testID={`queue-item-${index}`}
isNested
editing
/>
</Sortable.Touchable>
<Sortable.Touchable
onTap={async () => {
setQueue(queue.filter(({ item }) => item.Id !== queueItem.item.Id))
await removeFromQueue(index)
}}
>
<Icon name='close' color='$warning' />
</Sortable.Touchable>
</XStack>
),
[queueRef, skip, removeFromQueue],
<Sortable.Touchable
onTap={async () => {
setQueue(queue.filter(({ item }) => item.Id !== queueItem.item.Id))
await removeFromQueue(index)
}}
>
<Icon name='close' color='$warning' />
</Sortable.Touchable>
</XStack>
)
const handleReorder = async ({ fromIndex, toIndex }: OrderChangeParams) =>
await reorderQueue({ fromIndex, toIndex })
return (
<ScrollView flex={1} contentInsetAdjustmentBehavior='automatic'>
<Sortable.Grid
@@ -87,10 +99,8 @@ export default function Queue({
columns={1}
keyExtractor={keyExtractor}
renderItem={renderItem}
onOrderChange={reorderQueue}
onDragEnd={({ data }) => {
setQueue(data)
}}
onOrderChange={handleReorder}
onDragEnd={({ data }) => setQueue(data)}
overDrag='vertical'
customHandle
hapticsEnabled={!reducedHaptics}

View File

@@ -151,69 +151,72 @@ export default function Playlist({
const handleDownload = () => addToDownloadQueue(playlistTracks ?? [])
const editModeActions = (
<Animated.View
entering={FadeIn.springify()}
exiting={FadeOut.springify()}
layout={LinearTransition.springify()}
>
<XStack gap={'$2'}>
<Icon
color={'$warning'}
name='delete-sweep-outline' // otherwise use "delete-circle"
onPress={() => {
navigationRef.dispatch(
StackActions.push('DeletePlaylist', {
playlist,
onDelete: navigation.goBack,
}),
)
}}
/>
<Icon color='$neutral' name='close-circle-outline' onPress={handleCancel} />
</XStack>
</Animated.View>
)
const downloadActions = (
<XStack gap={'$2'}>
{playlistTracks &&
(isDownloaded ? (
<Animated.View
entering={FadeInUp.springify()}
exiting={FadeOutDown.springify()}
layout={LinearTransition.springify()}
>
<Icon color='$warning' name='broom' onPress={handleDeleteDownload} />
</Animated.View>
) : playlistDownloadPending ? (
<Spinner justifyContent='center' color={'$neutral'} />
) : (
<Animated.View
entering={FadeInUp.springify()}
exiting={FadeOutDown.springify()}
layout={LinearTransition.springify()}
>
<Icon
color='$success'
name='download-circle-outline'
onPress={handleDownload}
/>
</Animated.View>
))}
</XStack>
)
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<XStack gap={'$2'}>
{playlistTracks &&
(isDownloaded ? (
<Animated.View
entering={FadeInUp.springify()}
exiting={FadeOutDown.springify()}
layout={LinearTransition.springify()}
>
<Icon
color='$warning'
name='broom'
onPress={handleDeleteDownload}
/>
</Animated.View>
) : playlistDownloadPending ? (
<Spinner justifyContent='center' color={'$neutral'} />
) : (
<Animated.View
entering={FadeInUp.springify()}
exiting={FadeOutDown.springify()}
layout={LinearTransition.springify()}
>
<Icon
color='$success'
name='download-circle-outline'
onPress={handleDownload}
/>
</Animated.View>
))}
{canEdit &&
(editing ? (
<Animated.View
entering={FadeIn.springify()}
exiting={FadeOut.springify()}
layout={LinearTransition.springify()}
>
<XStack gap={'$2'}>
<Icon
color={'$warning'}
name='delete-sweep-outline' // otherwise use "delete-circle"
onPress={() => {
navigationRef.dispatch(
StackActions.push('DeletePlaylist', {
playlist,
onDelete: navigation.goBack,
}),
)
}}
/>
<Icon
color='$neutral'
name='close-circle-outline'
onPress={handleCancel}
/>
</XStack>
</Animated.View>
) : isUpdating || isPreparingEditMode ? (
<Spinner color={isPreparingEditMode ? '$primary' : '$success'} />
) : (
{playlistTracks && !editing && downloadActions}
{canEdit && (
<XStack gap={'$2'}>
{editing ? (
editModeActions
) : isUpdating || isPreparingEditMode ? (
<Spinner color={isPreparingEditMode ? '$primary' : '$success'} />
) : null}
<Animated.View
entering={FadeIn.springify()}
exiting={FadeOut.springify()}
@@ -233,8 +236,8 @@ export default function Playlist({
}
/>
</Animated.View>
))}
)
</XStack>
)}
</XStack>
),
})

View File

@@ -7,12 +7,11 @@ import { QueryKeys } from '../../enums/query-keys'
import { fetchSearchResults } from '../../api/queries/search'
import { useQuery } from '@tanstack/react-query'
import { FlatList } from 'react-native'
import { fetchSearchSuggestions } from '../../api/queries/suggestions/utils/suggestions'
import { getToken, H3, Separator, Spinner, YStack } from 'tamagui'
import Suggestions from './suggestions'
import { isEmpty } from 'lodash'
import HorizontalCardList from '../Global/components/horizontal-list'
import { ItemCard } from '../Global/components/item-card'
import ItemCard from '../Global/components/item-card'
import SearchParamList from '../../screens/Search/types'
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
import { useApi, useJellifyLibrary, useJellifyUser } from '../../stores'

View File

@@ -1,7 +1,7 @@
import ItemRow from '../Global/components/item-row'
import { Text } from '../Global/helpers/text'
import { H5, Separator, Spinner, YStack } from 'tamagui'
import { ItemCard } from '../Global/components/item-card'
import ItemCard from '../Global/components/item-card'
import HorizontalCardList from '../Global/components/horizontal-list'
import { FlashList } from '@shopify/flash-list'
import SearchParamList from '../../screens/Search/types'

View File

@@ -1,4 +1,29 @@
import axios from 'axios'
import axios, { AxiosAdapter } from 'axios'
import { fetch } from 'react-native-nitro-fetch'
const nitroAxiosAdapter: AxiosAdapter = async (config) => {
const response = await fetch(config.url!, {
method: config.method?.toUpperCase(),
headers: config.headers,
body: config.data,
})
const data = await response.json()
const headers: Record<string, string> = {}
response.headers.forEach((value, key) => {
headers[key] = value
})
return {
data,
status: response.status,
statusText: response.statusText,
headers,
config,
request: null,
}
}
/**
* The Axios instance for making HTTP requests.
@@ -7,6 +32,7 @@ import axios from 'axios'
*/
const AXIOS_INSTANCE = axios.create({
timeout: 60000,
adapter: nitroAxiosAdapter,
})
export default AXIOS_INSTANCE

View File

@@ -226,9 +226,11 @@ export const useRemoveFromQueue = () => {
return async (index: number) => {
trigger('impactMedium')
TrackPlayer.remove([index])
const newQueue = await TrackPlayer.getQueue()
usePlayerQueueStore.getState().setQueue(newQueue as JellifyTrack[])
const prevQueue = usePlayerQueueStore.getState().queue
const newQueue = prevQueue.filter((_, i) => i !== index)
usePlayerQueueStore.getState().setQueue(newQueue)
}
}
@@ -244,9 +246,15 @@ export const useRemoveUpcomingTracks = () => {
export const useReorderQueue = () => {
return async ({ fromIndex, toIndex }: QueueOrderMutation) => {
await TrackPlayer.move(fromIndex, toIndex)
const newQueue = await TrackPlayer.getQueue()
usePlayerQueueStore.getState().setQueue(newQueue as JellifyTrack[])
const queue = usePlayerQueueStore.getState().queue
const itemToMove = queue[fromIndex]
const newQueue = [...queue]
newQueue.splice(fromIndex, 1)
newQueue.splice(toIndex, 0, itemToMove)
usePlayerQueueStore.getState().setQueue(newQueue)
}
}

View File

@@ -1,10 +1,10 @@
import ArtistNavigation from '../../components/Artist'
import { ArtistProvider } from '../../providers/Artist'
import { RouteProp } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../types'
import { ArtistProvider } from '../../providers/Artist'
import ArtistNavigation from '../../components/Artist'
export function ArtistScreen({
export default function ArtistScreen({
route,
navigation,
}: {

View File

@@ -1,7 +1,7 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import Index from '../../components/Discover/component'
import AlbumScreen from '../Album'
import { ArtistScreen } from '../Artist'
import ArtistScreen from '../Artist'
import { getTokenValue, useTheme } from 'tamagui'
import RecentlyAdded from './albums'
import PublicPlaylists from './playlists'
@@ -10,6 +10,7 @@ import SuggestedArtists from './artists'
import DiscoverStackParamList from './types'
import InstantMix from '../../components/InstantMix/component'
import { getItemName } from '../../utils/text'
import TracksScreen from '../Tracks'
export const DiscoverStack = createNativeStackNavigator<DiscoverStackParamList>()
@@ -101,6 +102,8 @@ export function Discover(): React.JSX.Element {
headerTitle: `${getItemName(route.params.item)} Mix`,
})}
/>
<DiscoverStack.Screen name='Tracks' component={TracksScreen} />
</DiscoverStack.Navigator>
)
}

View File

@@ -2,7 +2,7 @@ import _ from 'lodash'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { PlaylistScreen } from '../Playlist'
import { Home as HomeComponent } from '../../components/Home'
import { ArtistScreen } from '../Artist'
import ArtistScreen from '../Artist'
import { getTokenValue, useTheme } from 'tamagui'
import HomeArtistsScreen from './artists'
import HomeTracksScreen from './tracks'
@@ -10,6 +10,7 @@ import AlbumScreen from '../Album'
import HomeStackParamList from './types'
import InstantMix from '../../components/InstantMix/component'
import { getItemName } from '../../utils/text'
import TracksScreen from '../Tracks'
const HomeStack = createNativeStackNavigator<HomeStackParamList>()
@@ -99,6 +100,8 @@ export default function Home(): React.JSX.Element {
headerTitle: `${getItemName(route.params.item)} Mix`,
})}
/>
<HomeStack.Screen name='Tracks' component={TracksScreen} />
</HomeStack.Group>
</HomeStack.Navigator>
)

View File

@@ -2,7 +2,7 @@ import React from 'react'
import Library from '../../components/Library/component'
import { PlaylistScreen } from '../Playlist'
import AddPlaylist from './add-playlist'
import { ArtistScreen } from '../Artist'
import ArtistScreen from '../Artist'
import { useTheme } from 'tamagui'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import AlbumScreen from '../Album'
@@ -10,6 +10,7 @@ import LibraryStackParamList from './types'
import InstantMix from '../../components/InstantMix/component'
import { getItemName } from '../../utils/text'
import { Platform } from 'react-native'
import TracksScreen from '../Tracks'
const LibraryStack = createNativeStackNavigator<LibraryStackParamList>()
@@ -81,6 +82,8 @@ export default function LibraryScreen(): React.JSX.Element {
sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [0.5],
}}
/>
<LibraryStack.Screen name='Tracks' component={TracksScreen} />
</LibraryStack.Navigator>
)
}

View File

@@ -1,5 +1,5 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { ArtistScreen } from '../Artist'
import ArtistScreen from '../Artist'
import AlbumScreen from '../Album'
import { PlaylistScreen } from '../Playlist'
import { getTokenValue, useTheme } from 'tamagui'
@@ -7,6 +7,7 @@ import Search from '../../components/Search'
import SearchParamList from './types'
import InstantMix from '../../components/InstantMix/component'
import { getItemName } from '../../utils/text'
import TracksScreen from '../Tracks'
const Stack = createNativeStackNavigator<SearchParamList>()
@@ -68,6 +69,8 @@ export default function SearchStack(): React.JSX.Element {
headerTitle: `${getItemName(route.params.item)} Mix`,
})}
/>
<Stack.Screen name='Tracks' component={TracksScreen} />
</Stack.Navigator>
)
}

View File

@@ -36,7 +36,7 @@ export type BaseStackParamList = {
}
Tracks: {
tracksInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
tracksInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
}
}