mirror of
https://github.com/Jellify-Music/App.git
synced 2026-05-23 21:39:03 -05:00
navigational and styling fixes
This commit is contained in:
@@ -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
@@ -0,0 +1,6 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export interface AddToPlaylistMutation {
|
||||
track: BaseItemDto
|
||||
playlist: BaseItemDto
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -42,6 +42,7 @@ export default function Playlists({
|
||||
navigation.navigate('Playlist', { playlist, canEdit })
|
||||
}}
|
||||
queueName={playlist.Name ?? 'Untitled Playlist'}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
onEndReached={() => {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -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<
|
||||
|
||||
Reference in New Issue
Block a user