Merge branch 'feature/nitro-player' of github.com:Jellify-Music/App into feature/nitro-player

This commit is contained in:
Violet Caulfield
2026-02-27 07:37:58 -06:00
9 changed files with 129 additions and 67 deletions

View File

@@ -0,0 +1,50 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { ListItem } from 'tamagui'
import Icon from '../../Global/components/icon'
import { Text } from '../../Global/helpers/text'
import { useMutation } from '@tanstack/react-query'
import { removeFromPlaylist } from '../../../api/mutations/playlists'
import { useApi } from '../../../stores'
import navigationRef from '../../../../navigation'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
import { queryClient } from '../../../constants/query-client'
import { PlaylistTracksQueryKey } from '../../../api/queries/playlist/keys'
export default function RemoveFromPlaylistRow({
track,
playlist,
}: {
track: BaseItemDto
playlist: BaseItemDto
}): React.JSX.Element {
const api = useApi()
const { mutate: removeTrack, isPending } = useMutation({
mutationFn: () => removeFromPlaylist(api, track, playlist),
onSuccess: () => {
triggerHaptic('notificationSuccess')
queryClient.invalidateQueries({ queryKey: PlaylistTracksQueryKey(playlist) })
navigationRef.goBack()
},
onError: () => {
triggerHaptic('notificationError')
},
})
return (
<ListItem
animation={'quick'}
backgroundColor={'transparent'}
disabled={isPending}
flex={1}
gap={'$2.5'}
justifyContent='flex-start'
onPress={() => removeTrack()}
pressStyle={{ opacity: 0.5 }}
>
<Icon small color='$warning' name='playlist-remove' />
<Text bold>Remove from Playlist</Text>
</ListItem>
)
}

View File

@@ -28,6 +28,7 @@ import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { Platform } from 'react-native'
import { useApi } from '../../stores'
import DeletePlaylistRow from './components/delete-playlist-row'
import RemoveFromPlaylistRow from './components/remove-from-playlist-row'
import useDownloadTracks, { useDeleteDownloads } from '../../hooks/downloads/mutations'
import { useIsDownloaded } from '../../hooks/downloads'
import { useDownloadProgress } from 'react-native-nitro-player'
@@ -37,6 +38,7 @@ type StackNavigation = Pick<NativeStackNavigationProp<BaseStackParamList>, 'navi
interface ContextProps {
item: BaseItemDto
playlist?: BaseItemDto
streamingMediaSourceInfo?: MediaSourceInfo
downloadedMediaSourceInfo?: MediaSourceInfo
stackNavigation?: StackNavigation
@@ -46,6 +48,7 @@ interface ContextProps {
export default function ItemContext({
item,
playlist,
streamingMediaSourceInfo,
downloadedMediaSourceInfo,
stackNavigation,
@@ -85,6 +88,8 @@ export default function ItemContext({
const renderAddToPlaylistRow = isTrack || isAlbum
const renderRemoveFromPlaylistRow = isTrack && !!playlist
const renderViewAlbumRow = isAlbum || (isTrack && album)
const renderDeletePlaylistRow = isPlaylist && item.CanDelete
@@ -107,15 +112,13 @@ export default function ItemContext({
useEffect(() => triggerHaptic('impactLight'), [item?.Id])
return (
<YGroup scrollable={Platform.OS === 'android'} marginBottom={'$8'}>
<YGroup scrollable={Platform.OS === 'android'} marginBottom={'$3'}>
<FavoriteContextMenuRow item={item} />
{renderDeletePlaylistRow && <DeletePlaylistRow playlist={item} />}
{renderAddToQueueRow && <AddToQueueMenuRow tracks={itemTracks} />}
{renderAddToQueueRow && <DownloadMenuRow items={itemTracks} />}
{renderAddToPlaylistRow && (
<AddToPlaylistRow
tracks={isAlbum && discs ? discs.flatMap((d) => d.data) : [item]}
@@ -123,6 +126,12 @@ export default function ItemContext({
/>
)}
{renderAddToQueueRow && <DownloadMenuRow items={itemTracks} />}
{renderRemoveFromPlaylistRow && playlist && (
<RemoveFromPlaylistRow track={item} playlist={playlist} />
)}
{(streamingMediaSourceInfo || downloadedMediaSourceInfo) && (
<StatsRow
item={item}

View File

@@ -19,7 +19,6 @@ export interface TrackRowContentProps {
textColor?: string
indexNumber: string
trackName: string
shouldShowArtists: boolean
artistsText: string
runtimeComponent: React.ReactNode
editing?: boolean
@@ -90,7 +89,6 @@ export default function TrackRowContent({
textColor,
indexNumber,
trackName,
shouldShowArtists,
artistsText,
runtimeComponent,
editing,
@@ -129,7 +127,7 @@ export default function TrackRowContent({
<Text
key={`${track.Id}-number`}
marginHorizontal={'auto'}
minWidth={'$4'}
minWidth={'$3'}
color={textColor}
textAlign='center'
fontVariant={['tabular-nums']}
@@ -140,7 +138,7 @@ export default function TrackRowContent({
</XStack>
<SlidingTextArea leftGapWidth={artworkAreaWidth} hasArtwork={!!showArtwork}>
<YStack alignItems='flex-start' justifyContent='center'>
<YStack alignItems='flex-start' justifyContent='center' gap={'$0'}>
<XStack alignItems='center'>
<Text
key={`${track.Id}-name`}
@@ -148,41 +146,34 @@ export default function TrackRowContent({
color={textColor}
lineBreakStrategyIOS='standard'
numberOfLines={1}
fontSize={'$4'}
>
{trackName}
</Text>
{!shouldShowArtists && isExplicit(track) && (
<XStack alignSelf='center' paddingLeft='$2'>
</XStack>
<XStack alignItems='center' gap={'$1'}>
<DownloadedIcon item={track} size='xxxsmall' />
<Text
key={`${track.Id}-artists`}
lineBreakStrategyIOS='standard'
numberOfLines={1}
color={'$borderColor'}
fontSize={'$2'}
marginVertical={'$-1.5'}
>
{artistsText}
</Text>
{isExplicit(track) && (
<XStack alignSelf='center' paddingTop='0.5'>
<Icon
name='alpha-e-box-outline'
color={'$borderColor'}
xxsmall
xxxsmall
/>
</XStack>
)}
</XStack>
{shouldShowArtists && (
<XStack alignItems='center'>
<Text
key={`${track.Id}-artists`}
lineBreakStrategyIOS='standard'
numberOfLines={1}
color={'$borderColor'}
>
{artistsText}
</Text>
{isExplicit(track) && (
<XStack alignSelf='center' paddingTop='$1' paddingLeft='$1'>
<Icon
name='alpha-e-box-outline'
color={'$borderColor'}
xxsmall
/>
</XStack>
)}
</XStack>
)}
</YStack>
</SlidingTextArea>
@@ -193,7 +184,6 @@ export default function TrackRowContent({
flexShrink={1}
gap='$1'
>
<DownloadedIcon item={track} />
<FavoriteIcon item={track} />
{runtimeComponent}
{!editing && <Icon name={'dots-horizontal'} onPress={handleIconPress} />}

View File

@@ -27,6 +27,7 @@ export interface TrackProps {
tracklist?: BaseItemDto[] | undefined
index: number
queue: Queue
playlist?: BaseItemDto
showArtwork?: boolean | undefined
onPress?: () => Promise<void> | undefined
onLongPress?: () => void | undefined
@@ -45,6 +46,7 @@ export default function Track({
tracklist,
index,
queue,
playlist,
showArtwork,
onPress,
onLongPress,
@@ -104,6 +106,7 @@ export default function Track({
navigationRef.navigate('Context', {
item: track,
navigation,
...(playlist && { playlist }),
})
}
}
@@ -112,6 +115,7 @@ export default function Track({
navigationRef.navigate('Context', {
item: track,
navigation,
...(playlist && { playlist }),
})
}
@@ -140,9 +144,6 @@ export default function Track({
// Memoize index number
const indexNumber = track.IndexNumber?.toString() ?? ''
// Memoize show artists condition
const shouldShowArtists = showArtwork || (track.ArtistItems && track.ArtistItems.length > 1)
const swipeHandlers = {
addToQueue: async () => {
console.info('Running add to queue swipe action')
@@ -196,7 +197,6 @@ export default function Track({
textColor={textColor}
indexNumber={indexNumber}
trackName={trackName}
shouldShowArtists={shouldShowArtists ?? false}
artistsText={artistsText}
runtimeComponent={runtimeComponent}
editing={editing}
@@ -222,7 +222,6 @@ export default function Track({
textColor={textColor}
indexNumber={indexNumber}
trackName={trackName}
shouldShowArtists={shouldShowArtists ?? false}
artistsText={artistsText}
runtimeComponent={runtimeComponent}
editing={editing}

View File

@@ -5,28 +5,38 @@ import { useIsDownloaded } from '../../../hooks/downloads'
import { useDownloadProgress } from 'react-native-nitro-player'
import CircularProgressIndicator from './circular-progress-indicator'
function DownloadedIcon({ item }: { item: BaseItemDto }) {
function DownloadedIcon({
item,
size = 'small',
}: {
item: BaseItemDto
size?: 'xxxsmall' | 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large'
}) {
const isDownloaded = useIsDownloaded([item.Id])
const { overallProgress } = useDownloadProgress({
const { overallProgress, isDownloading } = useDownloadProgress({
trackIds: [item.Id!],
activeOnly: true,
})
return isDownloaded ? (
<Animated.View
entering={FadeIn.easing(Easing.in(Easing.ease))}
exiting={FadeOut.easing(Easing.out(Easing.ease))}
>
<Icon small name='download-circle' color={'$success'} flex={1} />
</Animated.View>
return isDownloaded || isDownloading ? (
isDownloaded ? (
<Animated.View
entering={FadeIn.easing(Easing.in(Easing.ease))}
exiting={FadeOut.easing(Easing.out(Easing.ease))}
>
<Icon {...{ [size]: true }} name='download-circle' color={'$success'} flex={1} />
</Animated.View>
) : (
<Animated.View
entering={FadeIn.easing(Easing.in(Easing.ease))}
exiting={FadeOut.easing(Easing.out(Easing.ease))}
>
<CircularProgressIndicator progress={overallProgress} size={12} strokeWidth={4} />
</Animated.View>
)
) : (
<Animated.View
entering={FadeIn.easing(Easing.in(Easing.ease))}
exiting={FadeOut.easing(Easing.out(Easing.ease))}
>
<CircularProgressIndicator progress={overallProgress} size={24} strokeWidth={4} />
</Animated.View>
<></>
)
}

View File

@@ -12,22 +12,27 @@ import {
YStack,
} from 'tamagui'
import MaterialDesignIcon from '@react-native-vector-icons/material-design-icons'
import { on } from 'events'
const xxxsmallSize = 12
const xxsmallSize = 16
const xsmallSize = 20
const smallSize = 28
const regularSize = 34
const largeSize = 44
const SIZE_ENTRIES = [
['large', largeSize],
['small', smallSize],
['xsmall', xsmallSize],
['xxsmall', xxsmallSize],
['xxxsmall', xxxsmallSize],
] as const
export default function Icon({
name,
onPress,
onPressIn,
xxxsmall,
xxsmall,
xsmall,
small,
@@ -40,6 +45,7 @@ export default function Icon({
name: string
onPress?: () => void
onPressIn?: () => void
xxxsmall?: boolean
xxsmall?: boolean
xsmall?: boolean
small?: boolean
@@ -50,15 +56,8 @@ export default function Icon({
testID?: string | undefined
}): React.JSX.Element {
const theme = useTheme()
const size = large
? largeSize
: small
? smallSize
: xsmall
? xsmallSize
: xxsmall
? xxsmallSize
: regularSize
const sizeProps = { large, small, xsmall, xxsmall, xxxsmall }
const size = SIZE_ENTRIES.find(([key]) => sizeProps[key])?.[1] ?? regularSize
const animation = onPress || onPressIn ? 'quick' : undefined

View File

@@ -297,6 +297,7 @@ export default function Playlist({
rootNavigation.navigate('Context', {
item: track,
navigation,
playlist,
})
}}
>
@@ -306,6 +307,7 @@ export default function Playlist({
tracklist={playlistTracks ?? []}
index={index}
queue={playlist}
playlist={playlist}
showArtwork
editing={editing}
/>
@@ -333,6 +335,7 @@ export default function Playlist({
tracklist={playlistTracks ?? []}
index={index}
queue={playlist}
playlist={playlist}
showArtwork
/>
)

View File

@@ -6,6 +6,7 @@ export default function ItemContextScreen({ route, navigation }: ContextProps):
<ItemContext
navigation={navigation}
item={route.params.item}
playlist={route.params.playlist}
stackNavigation={route.params.navigation}
navigationCallback={route.params.navigationCallback}
streamingMediaSourceInfo={route.params.streamingMediaSourceInfo}

View File

@@ -54,6 +54,7 @@ export type RootStackParamList = {
Context: {
item: BaseItemDto
playlist?: BaseItemDto
streamingMediaSourceInfo?: MediaSourceInfo
downloadedMediaSourceInfo?: MediaSourceInfo
navigation?: Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>