navigational and styling fixes

This commit is contained in:
Violet Caulfield
2025-08-15 19:26:42 -05:00
parent 6ccd88c0a6
commit 91322b426b
26 changed files with 546 additions and 244 deletions
+182
View File
@@ -0,0 +1,182 @@
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'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { useMemo } from 'react'
import { trigger } from 'react-native-haptic-feedback'
import Toast from 'react-native-toast-message'
import {
YStack,
XStack,
Spacer,
Spinner,
YGroup,
Separator,
ListItem,
getTokens,
ScrollView,
} from 'tamagui'
import Icon from '../Global/components/icon'
import { AddToPlaylistMutation } from './types'
import { Text } from '../Global/helpers/text'
import ItemImage from '../Global/components/image'
import TextTicker from 'react-native-text-ticker'
import { TextTickerConfig } from '../Player/component.config'
export default function AddToPlaylist({ track }: { track: BaseItemDto }): React.JSX.Element {
const { api, user, library } = useJellifyContext()
const {
data: playlists,
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
},
})
// Fetch all playlist tracks to check if the current track is already in any playlists
const playlistsWithTracks = useQuery({
queryKey: [QueryKeys.PlaylistItemCheckCache, playlists?.map((p) => p.Id).join(',')],
enabled: !!playlists && playlists.length > 0,
queryFn: () => {
console.debug('Fetching playlist contents')
return Promise.all(
playlists!.map(async (playlist) => {
const response = await getItemsApi(api!).getItems({
parentId: playlist.Id!,
})
return {
playlistId: playlist.Id!,
tracks: response.data.Items || [],
}
}),
)
},
})
// Check if a track is in a playlist
const isTrackInPlaylist = useMemo(() => {
if (!playlistsWithTracks.data) return {}
const result: Record<string, boolean> = {}
playlistsWithTracks.data.forEach((playlistData) => {
result[playlistData.playlistId] = playlistData.tracks.some(
(playlistTrack) => playlistTrack.Id === track.Id,
)
})
return result
}, [playlistsWithTracks.data, track.Id])
const useAddToPlaylist = useMutation({
mutationFn: ({ track, playlist }: AddToPlaylistMutation) => {
trigger('impactLight')
return addToPlaylist(api, user, track, playlist)
},
onSuccess: (data, { playlist }) => {
Toast.show({
text1: 'Added to playlist',
type: 'success',
})
trigger('notificationSuccess')
queryClient.invalidateQueries({
queryKey: [QueryKeys.Playlists],
})
queryClient.invalidateQueries({
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
})
// Invalidate our playlist check cache
queryClient.invalidateQueries({
queryKey: [QueryKeys.PlaylistItemCheckCache],
})
},
onError: () => {
Toast.show({
text1: 'Unable to add',
type: 'error',
})
trigger('notificationError')
},
})
return (
<ScrollView>
<XStack gap={'$2'} margin={'$4'}>
<ItemImage item={track} />
<YStack gap={'$2'}>
<TextTicker {...TextTickerConfig}></TextTicker>
</YStack>
</XStack>
{!playlistsFetchPending && playlistsFetchSuccess && (
<YGroup separator={<Separator />}>
{playlists?.map((playlist) => {
const isInPlaylist = isTrackInPlaylist[playlist.Id!]
return (
<YGroup.Item key={playlist.Id!}>
<ListItem
animation={'quick'}
disabled={isInPlaylist}
hoverTheme
opacity={isInPlaylist ? 0.7 : 1}
pressStyle={{ opacity: 0.5 }}
onPress={() => {
if (!isInPlaylist) {
useAddToPlaylist.mutate({
track,
playlist,
})
}
}}
>
<XStack alignItems='center' gap={'$2'}>
<ItemImage item={playlist} height={'$11'} width={'$11'} />
<YStack alignItems='flex-start' flex={5}>
<Text bold fontSize={'$6'}>
{playlist.Name ?? 'Untitled Playlist'}
</Text>
<Text color={getTokens().color.amethyst.val}>{`${
playlist.ChildCount ?? 0
} tracks`}</Text>
</YStack>
{isInPlaylist ? (
<Icon
flex={1}
name='check-circle-outline'
color={'$success'}
/>
) : (
<Spacer flex={1} />
)}
</XStack>
</ListItem>
</YGroup.Item>
)
})}
</YGroup>
)}
</ScrollView>
)
}
+6
View File
@@ -0,0 +1,6 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
export interface AddToPlaylistMutation {
track: BaseItemDto
playlist: BaseItemDto
}
+48 -32
View File
@@ -1,7 +1,6 @@
import { BaseStackParamList } from '../../screens/types'
import { YStack, XStack, Separator, getToken, Spacer, Spinner } from 'tamagui'
import { H5, Text } from '../Global/helpers/text'
import { ActivityIndicator, FlatList, SectionList } from 'react-native'
import { FlatList, SectionList } from 'react-native'
import { RunTimeTicks } from '../Global/helpers/time-codes'
import Track from '../Global/components/track'
import FavoriteButton from '../Global/components/favorite-button'
@@ -10,7 +9,7 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import InstantMixButton from '../Global/components/instant-mix-button'
import ItemImage from '../Global/components/image'
import React from 'react'
import React, { useCallback, useEffect, useMemo } from 'react'
import { useJellifyContext } from '../../providers'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import Icon from '../Global/components/icon'
@@ -21,10 +20,10 @@ import { useLoadQueueContext } from '../../providers/Player/queue'
import { QueuingType } from '../../enums/queuing-type'
import { useAlbumContext } from '../../providers/Album'
import { useNavigation } from '@react-navigation/native'
import { isUndefined } from 'lodash'
import HomeStackParamList from '@/src/screens/Home/types'
import LibraryStackParamList from '@/src/screens/Library/types'
import DiscoverStackParamList from '@/src/screens/Discover/types'
import { BaseStackParamList } from '@/src/screens/types'
/**
* The screen for an Album's track list
@@ -35,6 +34,8 @@ import DiscoverStackParamList from '@/src/screens/Discover/types'
* @returns A React component
*/
export function Album(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
const { album, discs, isPending } = useAlbumContext()
const { api, sessionId } = useJellifyContext()
@@ -51,47 +52,61 @@ export function Album(): React.JSX.Element {
useDownloadMultiple.mutate(jellifyTracks)
}
const playAlbum = (shuffled: boolean = false) => {
if (!discs || discs.length === 0) return
const playAlbum = useCallback(
(shuffled: boolean = false) => {
if (!discs || discs.length === 0) return
const allTracks = discs?.flatMap((disc) => disc.data) ?? []
if (allTracks.length === 0) return
const allTracks = discs.flatMap((disc) => disc.data) ?? []
if (allTracks.length === 0) return
useLoadNewQueue({
track: allTracks[0],
index: 0,
tracklist: allTracks,
queue: album,
queuingType: QueuingType.FromSelection,
shuffled,
startPlayback: true,
})
}
useLoadNewQueue({
track: allTracks[0],
index: 0,
tracklist: allTracks,
queue: album,
queuingType: QueuingType.FromSelection,
shuffled,
startPlayback: true,
})
},
[discs, useLoadNewQueue],
)
const sections = useMemo(
() =>
(Array.isArray(discs) ? discs : []).map(({ title, data }) => ({
title,
data: Array.isArray(data) ? data : [],
})),
[discs],
)
const hasMultipleSections = sections.length > 1
const albumTrackList = useMemo(() => discs?.flatMap((disc) => disc.data), [discs])
return (
<SectionList
contentInsetAdjustmentBehavior='automatic'
sections={!isUndefined(discs) ? discs : []}
sections={sections}
keyExtractor={(item, index) => item.Id! + index}
ItemSeparatorComponent={Separator}
renderSectionHeader={({ section }) => {
return (
return !isPending && hasMultipleSections ? (
<XStack
width='100%'
justifyContent={discs && discs?.length >= 2 ? 'space-between' : 'flex-end'}
justifyContent={hasMultipleSections ? 'space-between' : 'flex-end'}
alignItems='center'
backgroundColor={'$background'}
paddingHorizontal={'$4.5'}
>
{discs && discs.length >= 2 && (
<Text
paddingVertical={'$2'}
paddingLeft={'$4.5'}
bold
>{`Disc ${section.title}`}</Text>
)}
<Text
paddingVertical={'$2'}
paddingLeft={'$4.5'}
bold
>{`Disc ${section.title}`}</Text>
<Icon
name={pendingDownloads?.length ? 'progress-download' : 'download'}
name={pendingDownloads.length ? 'progress-download' : 'download'}
small
onPress={() => {
if (pendingDownloads.length) {
@@ -101,14 +116,15 @@ export function Album(): React.JSX.Element {
}}
/>
</XStack>
)
) : null
}}
ListHeaderComponent={() => AlbumTrackListHeader(album, playAlbum)}
renderItem={({ item: track, index }) => (
<Track
navigation={navigation}
track={track}
tracklist={discs?.flatMap((disc) => disc.data)}
index={discs?.flatMap((disc) => disc.data).indexOf(track) ?? index}
tracklist={albumTrackList}
index={albumTrackList?.indexOf(track) ?? index}
queue={album}
/>
)}
+9 -5
View File
@@ -1,5 +1,4 @@
import { ActivityIndicator, RefreshControl } from 'react-native'
import { useDisplayContext } from '../../providers/Display/display-provider'
import { getToken, Separator, XStack, YStack } from 'tamagui'
import React from 'react'
import { Text } from '../Global/helpers/text'
@@ -7,6 +6,9 @@ import { FlashList } from '@shopify/flash-list'
import { FetchNextPageOptions } from '@tanstack/react-query'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import ItemRow from '../Global/components/item-row'
import { useNavigation } from '@react-navigation/native'
import LibraryStackParamList from '../../screens/Library/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
interface AlbumsProps {
albums: (string | number | BaseItemDto)[] | undefined
@@ -24,9 +26,7 @@ export default function Albums({
isPending,
showAlphabeticalSelector,
}: AlbumsProps): React.JSX.Element {
useDisplayContext()
const itemHeight = getToken('$6')
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
const stickyHeaderIndices = React.useMemo(() => {
@@ -65,7 +65,11 @@ export default function Albums({
<Text>{album.toUpperCase()}</Text>
</XStack>
) : typeof album === 'number' ? null : typeof album === 'object' ? (
<ItemRow item={album} queueName={album.Name ?? 'Unknown Album'} />
<ItemRow
item={album}
queueName={album.Name ?? 'Unknown Album'}
navigation={navigation}
/>
) : null
}
ListEmptyComponent={
+6
View File
@@ -10,6 +10,9 @@ import { FlashList, FlashListRef } from '@shopify/flash-list'
import { AZScroller } from '../Global/components/alphabetical-selector'
import { useMutation } from '@tanstack/react-query'
import { isString } from 'lodash'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import LibraryStackParamList from '../../screens/Library/types'
/**
* @param artistsInfiniteQuery - The infinite query for artists
@@ -26,6 +29,8 @@ export default function Artists({
const theme = useTheme()
const { isFavorites } = useLibrarySortAndFilterContext()
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
const artists = artistsInfiniteQuery.data ?? []
const sectionListRef = useRef<FlashListRef<string | number | BaseItemDto>>(null)
@@ -142,6 +147,7 @@ export default function Artists({
circular
item={artist}
queueName={artist.Name ?? 'Unknown Artist'}
navigation={navigation}
/>
) : null
}
@@ -24,8 +24,8 @@ export default function MultipleArtists({
renderItem={({ item: artist }) => (
<ItemRow
circular
key={artist.Id}
item={artist}
key={artist.Id}
queueName={''}
onPress={() => {
navigation.popToTop()
+30 -4
View File
@@ -26,6 +26,7 @@ import navigationRef from '../../../navigation'
import { goToAlbumFromContextSheet, goToArtistFromContextSheet } from './utils/navigation'
import { getItemName } from '../../utils/text'
import ItemImage from '../Global/components/image'
import { StackActions } from '@react-navigation/native'
type StackNavigation = Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
@@ -87,6 +88,8 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re
const renderAddToQueueRow = isTrack || isAlbum || isPlaylist
const renderAddToPlaylistRow = isTrack
return (
<ZStack>
<ItemContextBackground item={item} />
@@ -98,9 +101,11 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re
<AddToQueueMenuRow tracks={isTrack ? [item] : tracks} />
)}
{renderAddToPlaylistRow && <AddToPlaylistRow track={item} />}
{!isArtist && !isPlaylist && (
<ViewAlbumMenuRow
item={isAlbum ? item : album!}
album={isAlbum ? item : album}
stackNavigation={stackNavigation}
/>
)}
@@ -139,6 +144,27 @@ function BackgroundBlur({ item }: { item: BaseItemDto }): React.JSX.Element {
)
}
function AddToPlaylistRow({ track }: { track: BaseItemDto }): React.JSX.Element {
return (
<ListItem
animation={'quick'}
backgroundColor={'transparent'}
flex={1}
gap={'$2'}
justifyContent='flex-start'
onPress={() => {
navigationRef.goBack()
navigationRef.dispatch(StackActions.push('AddToPlaylist', { track }))
}}
pressStyle={{ opacity: 0.5 }}
>
<Icon color='$primary' name='playlist-plus' />
<Text bold>Add to Playlist</Text>
</ListItem>
)
}
function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Element {
const useAddToQueue = useAddToQueueContext()
@@ -159,7 +185,7 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele
}}
pressStyle={{ opacity: 0.5 }}
>
<Icon color='$primary' name='playlist-plus' />
<Icon color='$primary' name='music-note-plus' />
<Text bold>Add to Queue</Text>
</ListItem>
@@ -182,11 +208,11 @@ function BackgroundGradient(): React.JSX.Element {
}
interface MenuRowProps {
item: BaseItemDto | undefined
album: BaseItemDto | undefined
stackNavigation?: StackNavigation
}
function ViewAlbumMenuRow({ item: album, stackNavigation }: MenuRowProps): React.JSX.Element {
function ViewAlbumMenuRow({ album: album, stackNavigation }: MenuRowProps): React.JSX.Element {
const goToAlbum = useCallback(() => {
if (stackNavigation && album) stackNavigation.navigate('Album', { album })
else goToAlbumFromContextSheet(album)
+1 -1
View File
@@ -86,7 +86,7 @@ function getBorderRadius(circular: boolean | undefined, width: Token | number |
: getTokenValue(width)
: getTokenValue('$12') + getTokenValue('$5')
} else if (!isUndefined(width)) {
borderRadius = typeof width === 'number' ? width / 16 : getTokenValue(width) / 16
borderRadius = typeof width === 'number' ? width / 10 : getTokenValue(width) / 10
}
return borderRadius
+18 -17
View File
@@ -1,6 +1,4 @@
import { BaseStackParamList, RootStackParamList } from '../../../screens/types'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { XStack, YStack } from 'tamagui'
import { Text } from '../helpers/text'
import Icon from './icon'
@@ -17,8 +15,17 @@ import { fetchMediaInfo } from '../../../api/queries/media'
import { QueryKeys } from '../../../enums/query-keys'
import { useJellifyContext } from '../../../providers'
import { useStreamingQualityContext } from '../../../providers/Settings'
import { useNavigation } from '@react-navigation/native'
import navigate from '../../../../navigation'
import navigationRef from '../../../../navigation'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../../screens/types'
interface ItemRowProps {
item: BaseItemDto
navigation?: Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
queueName: string
onPress?: () => void
circular?: boolean
}
/**
* Displays an item as a row in a list.
@@ -33,22 +40,15 @@ import navigate from '../../../../navigation'
*/
export default function ItemRow({
item,
navigation,
queueName,
onPress,
circular,
}: {
item: BaseItemDto
queueName: string
onPress?: () => void
circular?: boolean
}): React.JSX.Element {
}: ItemRowProps): React.JSX.Element {
const useLoadNewQueue = useLoadQueueContext()
const { api, user } = useJellifyContext()
const streamingQuality = useStreamingQualityContext()
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
useQuery({
queryKey: [QueryKeys.MediaSources, streamingQuality, item.Id],
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), item),
@@ -87,7 +87,7 @@ export default function ItemRow({
minHeight={'$7'}
width={'100%'}
onLongPress={() => {
rootNavigation.navigate('Context', {
navigationRef.navigate('Context', {
item,
navigation,
})
@@ -100,12 +100,12 @@ export default function ItemRow({
switch (item.Type) {
case 'MusicArtist': {
navigation.navigate('Artist', { artist: item })
navigation?.navigate('Artist', { artist: item })
break
}
case 'MusicAlbum': {
navigation.navigate('Album', { album: item })
navigation?.navigate('Album', { album: item })
break
}
}
@@ -163,8 +163,9 @@ export default function ItemRow({
<Icon
name='dots-horizontal'
onPress={() => {
rootNavigation.navigate('Context', {
navigationRef.navigate('Context', {
item,
navigation,
})
}}
/>
+10 -9
View File
@@ -1,11 +1,9 @@
import React, { useEffect } from 'react'
import React from 'react'
import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui'
import { Text } from '../helpers/text'
import { RunTimeTicks } from '../helpers/time-codes'
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import Icon from './icon'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList, RootStackParamList } from '../../../screens/types'
import { QueuingType } from '../../../enums/queuing-type'
import { Queue } from '../../../player/types/queue-item'
import FavoriteIcon from './favorite-icon'
@@ -22,10 +20,13 @@ import { fetchMediaInfo } from '../../../api/queries/media'
import { useStreamingQualityContext } from '../../../providers/Settings'
import { getQualityParams } from '../../../utils/mappings'
import { useNowPlayingContext } from '../../../providers/Player'
import { useNavigation } from '@react-navigation/native'
import navigationRef from '../../../../navigation'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '@/src/screens/types'
export interface TrackProps {
track: BaseItemDto
navigation: Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
tracklist?: BaseItemDto[] | undefined
index: number
queue: Queue
@@ -42,6 +43,7 @@ export interface TrackProps {
export default function Track({
track,
navigation,
tracklist,
index,
queue,
@@ -56,9 +58,6 @@ export default function Track({
}: TrackProps): React.JSX.Element {
const theme = useTheme()
const stackNavigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const { api, user } = useJellifyContext()
const nowPlaying = useNowPlayingContext()
const playQueue = usePlayQueueContext()
@@ -105,8 +104,9 @@ export default function Track({
onLongPress
? () => onLongPress()
: () => {
rootNavigation.navigate('Context', {
navigationRef.navigate('Context', {
item: track,
navigation,
})
}
}
@@ -204,8 +204,9 @@ export default function Track({
if (showRemove) {
if (onRemove) onRemove()
} else {
rootNavigation.navigate('Context', {
navigationRef.navigate('Context', {
item: track,
navigation,
})
}
}}
@@ -3,14 +3,20 @@ import React from 'react'
import Tracks from '../../Tracks/component'
import { useTracksInfiniteQueryContext } from '../../../providers/Library'
import { useLibrarySortAndFilterContext } from '../../../providers/Library/sorting-filtering'
import { useNavigation } from '@react-navigation/native'
import LibraryStackParamList from '@/src/screens/Library/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
function TracksTab(): React.JSX.Element {
const tracksInfiniteQuery = useTracksInfiniteQueryContext()
const { isFavorites, isDownloaded } = useLibrarySortAndFilterContext()
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
return (
<Tracks
navigation={navigation}
tracks={tracksInfiniteQuery.data}
queue={isFavorites ? 'Favorite Tracks' : isDownloaded ? 'Downloaded Tracks' : 'Library'}
filterDownloaded={isDownloaded}
+3 -13
View File
@@ -1,7 +1,7 @@
import { useJellifyContext } from '../../../providers'
import { useNowPlayingContext, usePlaybackStateContext } from '../../../providers/Player'
import { useQueueRefContext } from '../../../providers/Player/queue'
import { getToken, useWindowDimensions, XStack, YStack, useTheme } from 'tamagui'
import { getToken, useWindowDimensions, XStack, YStack, useTheme, Spacer } from 'tamagui'
import { Text } from '../../Global/helpers/text'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Icon from '../../Global/components/icon'
@@ -39,7 +39,7 @@ export default function PlayerHeader(): React.JSX.Element {
/>
</YStack>
<YStack alignItems='center' alignContent='center' flex={8}>
<YStack alignItems='center' alignContent='center' flex={2}>
<Text>Playing from</Text>
<Text bold numberOfLines={1} lineBreakStrategyIOS='standard'>
{
@@ -49,17 +49,7 @@ export default function PlayerHeader(): React.JSX.Element {
</Text>
</YStack>
<YStack flex={1} justifyContent='flex-end' alignContent='center'>
<Icon
small
name='dots-vertical'
onPress={() => {
navigation.navigate('Context', {
item: nowPlaying!.item,
})
}}
/>
</YStack>
<Spacer flex={1} />
</XStack>
<XStack justifyContent='center' alignContent='center' paddingVertical={'$8'}>
+2
View File
@@ -76,6 +76,7 @@ export default function Playlist({
{editing && canEdit && <Icon name='drag' onPress={drag} />}
<Track
navigation={navigation}
track={track}
tracklist={playlistTracks ?? []}
index={getIndex() ?? 0}
@@ -86,6 +87,7 @@ export default function Playlist({
? drag()
: rootNavigation.navigate('Context', {
item: track,
navigation,
})
}}
showRemove={editing}
+1
View File
@@ -42,6 +42,7 @@ export default function Playlists({
navigation.navigate('Playlist', { playlist, canEdit })
}}
queueName={playlist.Name ?? 'Untitled Playlist'}
navigation={navigation}
/>
)}
onEndReached={() => {
+3 -1
View File
@@ -112,7 +112,9 @@ export default function Search({
// We're displaying artists separately so we're going to filter them out here
data={items?.filter((result) => result.Type !== 'MusicArtist')}
refreshing={fetchingResults}
renderItem={({ item }) => <ItemRow item={item} queueName={searchString ?? 'Search'} />}
renderItem={({ item }) => (
<ItemRow item={item} queueName={searchString ?? 'Search'} navigation={navigation} />
)}
style={{
marginHorizontal: getToken('$2'),
marginTop: getToken('$4'),
+1 -1
View File
@@ -52,7 +52,7 @@ export default function Suggestions({
</Text>
}
renderItem={({ item }) => {
return <ItemRow item={item} queueName={'Suggestions'} />
return <ItemRow item={item} queueName={'Suggestions'} navigation={navigation} />
}}
style={{
marginHorizontal: 2,
+17 -10
View File
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'
import React from 'react'
import Track from '../Global/components/track'
import { getTokens, Separator } from 'tamagui'
import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models'
@@ -7,22 +7,28 @@ import { useNetworkContext } from '../../providers/Network'
import { queryClient } from '../../constants/query-client'
import { QueryKeys } from '../../enums/query-keys'
import { FlashList } from '@shopify/flash-list'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '@/src/screens/types'
export default function Tracks({
tracks,
queue,
fetchNextPage,
hasNextPage,
filterDownloaded,
filterFavorites,
}: {
interface TracksProps {
tracks: (string | number | BaseItemDto)[] | undefined
navigation: Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
queue: Queue
fetchNextPage: () => void
hasNextPage: boolean
filterDownloaded?: boolean | undefined
filterFavorites?: boolean | undefined
}): React.JSX.Element {
}
export default function Tracks({
tracks,
navigation,
queue,
fetchNextPage,
hasNextPage,
filterDownloaded,
filterFavorites,
}: TracksProps): React.JSX.Element {
const { downloadedTracks } = useNetworkContext()
// Memoize the expensive tracks processing to prevent memory leaks
@@ -56,6 +62,7 @@ export default function Tracks({
const renderItem = React.useCallback(
({ index, item: track }: { index: number; item: BaseItemDto }) => (
<Track
navigation={navigation}
showArtwork
index={0}
track={track}
+6 -10
View File
@@ -16,7 +16,7 @@ import {
import telemetryDeckConfig from '../../telemetrydeck.json'
import glitchtipConfig from '../../glitchtip.json'
import * as Sentry from '@sentry/react-native'
import { Theme, useTheme } from 'tamagui'
import { getToken, Theme, useTheme } from 'tamagui'
import Toast from 'react-native-toast-message'
import JellifyToastConfig from '../constants/toast.config'
import { useColorScheme } from 'react-native'
@@ -24,6 +24,7 @@ import { CarPlayProvider } from '../providers/CarPlay'
import { LibrarySortAndFilterProvider } from '../providers/Library/sorting-filtering'
import { LibraryProvider } from '../providers/Library'
import { HomeProvider } from '../providers/Home'
import { SafeAreaView } from 'react-native-safe-area-context'
/**
* The main component for the Jellify app. Children are wrapped in the {@link JellifyProvider}
* @returns The {@link Jellify} component
@@ -86,18 +87,13 @@ function App(): React.JSX.Element {
<NetworkContextProvider>
<QueueProvider>
<PlayerProvider>
<HomeProvider>
<LibrarySortAndFilterProvider>
<LibraryProvider>
<CarPlayProvider />
<Root />
</LibraryProvider>
</LibrarySortAndFilterProvider>
</HomeProvider>
<CarPlayProvider />
<Root />
</PlayerProvider>
</QueueProvider>
</NetworkContextProvider>
<Toast config={JellifyToastConfig(theme)} />
<Toast topOffset={getToken('$12')} config={JellifyToastConfig(theme)} />
</JellifyUserDataProvider>
)
}
+2 -1
View File
@@ -1,5 +1,6 @@
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'
import { BaseToast, BaseToastProps, ToastConfig } from 'react-native-toast-message'
import { getToken, ThemeParsed } from 'tamagui'
import { getToken, getTokenValue, ThemeParsed } from 'tamagui'
/**
* Configures the toast for the Jellify app, using Tamagui style tokens
+2 -3
View File
@@ -15,9 +15,8 @@ function AlbumContextInitializer(album: BaseItemDto): AlbumContext {
const { api } = useJellifyContext()
const { data: discs, isPending } = useQuery({
queryKey: [QueryKeys.ItemTracks, album.Id!],
queryKey: [QueryKeys.ItemTracks, album.Id],
queryFn: () => fetchAlbumDiscs(api, album),
enabled: true,
})
return {
@@ -29,7 +28,7 @@ function AlbumContextInitializer(album: BaseItemDto): AlbumContext {
const AlbumContext = createContext<AlbumContext>({
album: {},
discs: [],
discs: undefined,
isPending: false,
})
+6
View File
@@ -0,0 +1,6 @@
import AddToPlaylist from '../../components/AddToPlaylist/index'
import { AddToPlaylistProps } from '../types'
export default function AddToPlaylistSheet({ route }: AddToPlaylistProps): React.JSX.Element {
return <AddToPlaylist track={route.params.track} />
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { Album } from '../../components/Album'
import { AlbumProps } from '../types'
import { AlbumProvider } from '../../providers/Album'
export default function AlbumScreen({ route }: AlbumProps): React.JSX.Element {
export default function AlbumScreen({ route, navigation }: AlbumProps): React.JSX.Element {
const { album } = route.params
return (
+70 -65
View File
@@ -21,75 +21,80 @@ export default function Home(): React.JSX.Element {
const theme = useTheme()
return (
<HomeStack.Navigator initialRouteName='HomeScreen' screenOptions={{ headerShown: true }}>
<HomeStack.Group>
<HomeStack.Screen
name='HomeScreen'
component={ProvidedHome}
options={{
title: 'Home',
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',
},
})}
/>
<HomeProvider>
<HomeStack.Navigator
initialRouteName='HomeScreen'
screenOptions={{ headerShown: true }}
>
<HomeStack.Group>
<HomeStack.Screen
name='HomeScreen'
component={ProvidedHome}
options={{
title: 'Home',
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: true,
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
</HomeStack.Group>
</HomeStack.Navigator>
<HomeStack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
headerShown: true,
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
</HomeStack.Group>
</HomeStack.Navigator>
</HomeProvider>
)
}
+72 -66
View File
@@ -9,6 +9,8 @@ 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'
const Stack = createNativeStackNavigator<LibraryStackParamList>()
@@ -16,77 +18,81 @@ export default function LibraryStack({ route, navigation }: LibraryTabProps): Re
const theme = useTheme()
return (
<Stack.Navigator initialRouteName='LibraryScreen'>
<Stack.Screen
name='LibraryScreen'
component={LibraryScreen}
options={{
title: 'Library',
<LibrarySortAndFilterProvider>
<LibraryProvider>
<Stack.Navigator initialRouteName='LibraryScreen'>
<Stack.Screen
name='LibraryScreen'
component={LibraryScreen}
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,
}}
/>
// 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,
}}
/>
<Stack.Screen
name='Artist'
component={ArtistScreen}
options={({ route }) => ({
title: route.params.artist.Name ?? 'Unknown Artist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<Stack.Screen
name='Artist'
component={ArtistScreen}
options={({ route }) => ({
title: route.params.artist.Name ?? 'Unknown Artist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<Stack.Screen
name='Album'
component={AlbumScreen}
options={({ route }) => ({
headerShown: true,
title: route.params.album.Name ?? 'Untitled Album',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<Stack.Screen
name='Album'
component={AlbumScreen}
options={({ route }) => ({
headerShown: true,
title: route.params.album.Name ?? 'Untitled Album',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<Stack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
headerShown: true,
title: route.params.playlist.Name ?? 'Untitled Playlist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<Stack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
headerShown: true,
title: route.params.playlist.Name ?? 'Untitled Playlist',
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
<Stack.Group
screenOptions={{
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
}}
>
<Stack.Screen
name='AddPlaylist'
component={AddPlaylist}
options={{
title: 'Add Playlist',
}}
/>
<Stack.Group
screenOptions={{
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
}}
>
<Stack.Screen
name='AddPlaylist'
component={AddPlaylist}
options={{
title: 'Add Playlist',
}}
/>
<Stack.Screen
name='DeletePlaylist'
component={DeletePlaylist}
options={{
title: 'Delete Playlist',
}}
/>
</Stack.Group>
</Stack.Navigator>
<Stack.Screen
name='DeletePlaylist'
component={DeletePlaylist}
options={{
title: 'Delete Playlist',
}}
/>
</Stack.Group>
</Stack.Navigator>
</LibraryProvider>
</LibrarySortAndFilterProvider>
)
}
+38 -4
View File
@@ -8,6 +8,8 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
import Context from './Context'
import { getItemName } from '../utils/text'
import { useCallback } from 'react'
import AddToPlaylistSheet from './AddToPlaylist'
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
const RootStack = createNativeStackNavigator<RootStackParamList>()
@@ -16,9 +18,27 @@ export default function Root(): React.JSX.Element {
const { api, library } = useJellifyContext()
const getContextSheetDetents = useCallback((artists: string[] | null | undefined) => {
return [0.2 + (artists?.length ?? 1) * 0.1]
}, [])
const getContextSheetDetents = useCallback(
(artists: string[] | null | undefined, type: BaseItemKind | undefined) => {
let detent: number = 0
switch (type) {
case 'Audio':
detent = 0.25
break
case 'MusicAlbum':
detent = 0.2
break
case 'Playlist':
detent = 0.2
break
default:
detent = 0.15
}
return [detent + (artists?.length ?? 1) * 0.075]
},
[],
)
return (
<RootStack.Navigator
@@ -59,11 +79,25 @@ export default function Root(): React.JSX.Element {
options={({ route }) => ({
headerTitle: getItemName(route.params.item),
presentation: 'formSheet',
sheetAllowedDetents: getContextSheetDetents(route.params.item.Artists),
sheetAllowedDetents: getContextSheetDetents(
route.params.item.Artists,
route.params.item.Type,
),
sheetGrabberVisible: true,
headerTransparent: true,
})}
/>
<RootStack.Screen
name='AddToPlaylist'
component={AddToPlaylistSheet}
options={{
headerTitle: 'Add to Playlist',
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
sheetGrabberVisible: true,
}}
/>
</RootStack.Navigator>
)
}
+5
View File
@@ -61,12 +61,17 @@ export type RootStackParamList = {
navigation?: Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
navigationCallback?: (screen: 'Album' | 'Artist', item: BaseItemDto) => void
}
AddToPlaylist: {
track: BaseItemDto
}
}
export type LoginProps = NativeStackNavigationProp<RootStackParamList, 'Login'>
export type TabProps = NativeStackScreenProps<RootStackParamList, 'Tabs'>
export type PlayerProps = NativeStackScreenProps<RootStackParamList, 'PlayerRoot'>
export type ContextProps = NativeStackScreenProps<RootStackParamList, 'Context'>
export type AddToPlaylistProps = NativeStackScreenProps<RootStackParamList, 'AddToPlaylist'>
export type ArtistsProps = {
artistsInfiniteQuery: UseInfiniteQueryResult<