getting context sheet navigation to work

This commit is contained in:
Violet Caulfield
2025-08-15 11:32:13 -05:00
parent a2e3237766
commit 5a723bcefe
30 changed files with 353 additions and 301 deletions

View File

@@ -23,7 +23,7 @@ import ErrorBoundary from './src/components/ErrorBoundary'
import OTAUpdateScreen from './src/components/OtaUpdates'
import { usePerformanceMonitor } from './src/hooks/use-performance-monitor'
import { SettingsProvider, useThemeSettingContext } from './src/providers/Settings'
import { navigationRef } from './navigation'
import navigationRef from './navigation'
export default function App(): React.JSX.Element {
// Add performance monitoring to track app-level re-renders

View File

@@ -4,8 +4,9 @@ import App from './App'
import { name as appName } from './app.json'
import { PlaybackService } from './src/player/service'
import TrackPlayer from 'react-native-track-player'
import { enableFreeze } from 'react-native-screens'
import { enableFreeze, enableScreens } from 'react-native-screens'
enableScreens(true)
enableFreeze(true)
AppRegistry.registerComponent(appName, () => App)

View File

@@ -1,4 +1,6 @@
import { createNavigationContainerRef } from '@react-navigation/native'
import { RootStackParamList } from './src/screens/types'
export const navigationRef = createNavigationContainerRef<RootStackParamList>()
const navigationRef = createNavigationContainerRef<RootStackParamList>()
export default navigationRef

View File

@@ -73,7 +73,7 @@ export function Album(): React.JSX.Element {
contentInsetAdjustmentBehavior='automatic'
sections={!isUndefined(discs) ? discs : []}
keyExtractor={(item, index) => item.Id! + index}
ItemSeparatorComponent={() => <Separator />}
ItemSeparatorComponent={Separator}
renderSectionHeader={({ section }) => {
return (
<XStack

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'
import Albums from './albums'
import SimilarArtists from './similar'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
@@ -11,9 +11,11 @@ const ArtistTabs = createMaterialTopTabNavigator<ArtistTabList>()
export default function ArtistNavigation(): React.JSX.Element {
const { featuredOn, artist } = useArtistContext()
const hasFeaturedOn = useMemo(() => featuredOn && featuredOn.length > 0, [artist])
return (
<ArtistTabs.Navigator
tabBar={(props) => ArtistTabBar(props)}
tabBar={ArtistTabBar}
screenOptions={{
tabBarLabelStyle: {
fontFamily: 'Figtree-Bold',
@@ -36,7 +38,7 @@ export default function ArtistNavigation(): React.JSX.Element {
component={Albums}
/>
{featuredOn && featuredOn.length > 0 && (
{hasFeaturedOn && (
<ArtistTabs.Screen
name='ArtistFeaturedOn'
options={{

View File

@@ -6,8 +6,8 @@ import { RouteProp, useNavigation } from '@react-navigation/native'
import { RootStackParamList } from '../../../screens/types'
interface MultipleArtistsProps {
navigation: NativeStackNavigationProp<PlayerParamList, 'MultipleArtists'>
route: RouteProp<PlayerParamList, 'MultipleArtists'>
navigation: NativeStackNavigationProp<PlayerParamList, 'MultipleArtistsSheet'>
route: RouteProp<PlayerParamList, 'MultipleArtistsSheet'>
}
export default function MultipleArtists({
navigation,
@@ -27,7 +27,7 @@ export default function MultipleArtists({
navigation.popToTop()
rootNavigation.popTo('Tabs', {
screen: 'Library',
screen: 'LibraryTab',
params: {
screen: 'Artist',
params: {

View File

@@ -5,7 +5,7 @@ import { Text } from '../Global/helpers/text'
import FavoriteContextMenuRow from '../Global/components/favorite-context-menu-row'
import { Blurhash } from 'react-native-blurhash'
import { getPrimaryBlurhashFromDto } from '../../utils/blurhash'
import { useColorScheme } from 'react-native'
import { InteractionManager, useColorScheme } from 'react-native'
import { useThemeSettingContext } from '../../providers/Settings'
import LinearGradient from 'react-native-linear-gradient'
import Icon from '../Global/components/icon'
@@ -18,10 +18,13 @@ import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { useAddToQueueContext } from '../../providers/Player/queue'
import { AddToQueueMutation } from '../../providers/Player/interfaces'
import { QueuingType } from '../../enums/queuing-type'
import LibraryStackParamList, { LibraryNavigation } from '../../screens/Library/types'
import LibraryStackParamList from '../../screens/Library/types'
import DiscoverStackParamList from '../../screens/Discover/types'
import HomeStackParamList from '../../screens/Home/types'
import { useCallback } from 'react'
import { StackActions, TabActions } from '@react-navigation/native'
import navigationRef from '../../../navigation'
import { goToAlbumFromContextSheet, goToArtistFromContextSheet } from './utils/navigation'
interface ContextProps {
item: BaseItemDto
@@ -29,14 +32,11 @@ interface ContextProps {
HomeStackParamList | LibraryStackParamList | DiscoverStackParamList
>
navigation: NativeStackNavigationProp<RootStackParamList>
navigationCallback?: (screen: 'Album' | 'Artist', item: BaseItemDto) => void
}
export default function ItemContext({
item,
stackNavigation,
navigation,
}: ContextProps): React.JSX.Element {
const { api, user, library } = useJellifyContext()
export default function ItemContext({ item, stackNavigation }: ContextProps): React.JSX.Element {
const { api } = useJellifyContext()
const isArtist = item.Type === BaseItemKind.MusicArtist
const isAlbum = item.Type === BaseItemKind.MusicAlbum
@@ -45,19 +45,19 @@ export default function ItemContext({
const albumArtists = item.AlbumArtists ?? []
const { data: album, isSuccess: albumFetchSuccess } = useQuery({
const { data: album } = useQuery({
queryKey: [QueryKeys.Item, item.AlbumId],
queryFn: () => fetchItem(api, item.AlbumId!),
enabled: isTrack,
})
const { data: artist, isSuccess: artistFetchSuccess } = useQuery({
const { data: artist } = useQuery({
queryKey: [QueryKeys.ArtistById, albumArtists.length > 0 ? albumArtists[0].Id : item.Id],
queryFn: () => fetchItem(api, albumArtists[0].Id!),
enabled: (isTrack || isAlbum) && albumArtists.length > 0,
})
const { data: tracks, isSuccess: tracksFetchSuccess } = useQuery({
const { data: tracks } = useQuery({
queryKey: [QueryKeys.ItemTracks, item.Id],
queryFn: () =>
getItemsApi(api!)
@@ -85,7 +85,6 @@ export default function ItemContext({
<ViewAlbumMenuRow
item={isAlbum ? item : album!}
stackNavigation={stackNavigation}
rootNavigation={navigation}
/>
)}
@@ -93,7 +92,6 @@ export default function ItemContext({
<ViewArtistMenuRow
item={isArtist ? item : artist}
stackNavigation={stackNavigation}
rootNavigation={navigation}
/>
)}
</YGroup>
@@ -171,25 +169,13 @@ interface MenuRowProps {
stackNavigation?: NativeStackNavigationProp<
HomeStackParamList | LibraryStackParamList | DiscoverStackParamList
>
rootNavigation: NativeStackNavigationProp<RootStackParamList>
}
function ViewAlbumMenuRow({
item: album,
stackNavigation,
rootNavigation,
}: MenuRowProps): React.JSX.Element {
function ViewAlbumMenuRow({ item: album, stackNavigation }: MenuRowProps): React.JSX.Element {
const goToAlbum = useCallback(() => {
if (stackNavigation && album) stackNavigation.navigate('Album', { album })
else if (album) {
rootNavigation.popTo('Tabs', {
screen: 'Library',
merge: true,
})
LibraryNavigation.album = album
}
}, [album, stackNavigation, rootNavigation])
else goToAlbumFromContextSheet(album)
}, [album, stackNavigation, navigationRef])
return (
<ListItem
@@ -207,22 +193,11 @@ function ViewAlbumMenuRow({
)
}
function ViewArtistMenuRow({
item: artist,
stackNavigation,
rootNavigation,
}: MenuRowProps): React.JSX.Element {
function ViewArtistMenuRow({ item: artist, stackNavigation }: MenuRowProps): React.JSX.Element {
const goToArtist = useCallback(() => {
if (stackNavigation && artist) stackNavigation.navigate('Artist', { artist })
else if (artist) {
rootNavigation.popTo('Tabs', {
screen: 'Library',
merge: true,
})
LibraryNavigation.artist = artist
}
}, [artist, stackNavigation, rootNavigation])
else goToArtistFromContextSheet(artist)
}, [artist, stackNavigation, navigationRef])
return (
<ListItem

View File

@@ -0,0 +1,36 @@
import { CommonActions, StackActions, TabActions } from '@react-navigation/native'
import navigationRef from '../../../../navigation'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { InteractionManager } from 'react-native'
export function goToAlbumFromContextSheet(album: BaseItemDto | undefined) {
if (!navigationRef.isReady() || !album) return
// Pop Context Sheet and Player Modal
navigationRef.dispatch(StackActions.popTo('Tabs'))
const route = navigationRef.current?.getCurrentRoute()
if (route?.name.includes('Settings')) {
navigationRef.dispatch(TabActions.jumpTo('LibraryTab'))
requestAnimationFrame(() => {
navigationRef.dispatch(CommonActions.navigate('Album', { album }))
})
} else navigationRef.dispatch(CommonActions.navigate('Album', { album }))
}
export function goToArtistFromContextSheet(artist: BaseItemDto | undefined) {
if (!navigationRef.isReady() || !artist) return
// Pop Context Sheet and Player Modal
navigationRef.dispatch(StackActions.popTo('Tabs'))
const route = navigationRef.current?.getCurrentRoute()
if (route?.name.includes('Settings')) {
navigationRef.dispatch(TabActions.jumpTo('LibraryTab'))
requestAnimationFrame(() => {
navigationRef.dispatch(CommonActions.navigate('Artist', { artist }))
})
} else navigationRef.dispatch(CommonActions.navigate('Artist', { artist }))
}

View File

@@ -6,14 +6,11 @@ import { useHomeContext } from '../../providers/Home'
import FrequentArtists from './helpers/frequent-artists'
import FrequentlyPlayedTracks from './helpers/frequent-tracks'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useJellifyContext } from '../../providers'
import { usePreventRemove } from '@react-navigation/native'
export function ProvidedHome(): React.JSX.Element {
usePreventRemove(true, () => {})
const { user } = useJellifyContext()
const { refreshing: refetching, onRefresh } = useHomeContext()
const insets = useSafeAreaInsets()
return (
<ScrollView

View File

@@ -11,7 +11,10 @@ import React from 'react'
const LibraryTabsNavigator = createMaterialTopTabNavigator()
export default function Library({ route, navigation }: LibraryScreenProps): React.JSX.Element {
export default function LibraryScreen({
route,
navigation,
}: LibraryScreenProps): React.JSX.Element {
const theme = useTheme()
return (

View File

@@ -16,6 +16,7 @@ import Icon from '../../Global/components/icon'
import { useNavigation } from '@react-navigation/native'
import { QueryKeys } from '../../../enums/query-keys'
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
import { PlayerParamList } from '../../../screens/Player/types'
export default function SongInfo(): React.JSX.Element {
const { api, user, library } = useJellifyContext()
@@ -28,11 +29,7 @@ export default function SongInfo(): React.JSX.Element {
queryFn: () => fetchItem(api, nowPlaying!.item.AlbumId!),
})
const { data: artists } = useQuery<
{ title: string | number; data: BaseItemDto[] },
Error,
void
>({
useQuery({
queryKey: [QueryKeys.TrackArtists, nowPlaying!.item.ArtistItems],
queryFn: () => fetchItems(api, user, library, [BaseItemKind.MusicArtist]),
select: (data: { title: string | number; data: BaseItemDto[] }) => data.data,
@@ -46,7 +43,7 @@ export default function SongInfo(): React.JSX.Element {
onPress={() => {
if (album) {
navigation.popTo('Tabs', {
screen: 'Library',
screen: 'LibraryTab',
params: {
screen: 'Album',
params: {
@@ -86,12 +83,15 @@ export default function SongInfo(): React.JSX.Element {
onPress={() => {
if (nowPlaying!.item.ArtistItems) {
if (nowPlaying!.item.ArtistItems!.length > 1) {
navigation.navigate('MultipleArtists', {
artists: nowPlaying!.item.ArtistItems!,
navigation.navigate('PlayerRoot', {
screen: 'MultipleArtistsSheet',
params: {
artists: nowPlaying!.item.ArtistItems!,
},
})
} else {
navigation.popTo('Tabs', {
screen: 'Library',
screen: 'LibraryTab',
params: {
screen: 'Artist',
params: {

View File

@@ -11,7 +11,7 @@ import {
} from 'tamagui'
import { useNowPlayingContext } from '../../providers/Player'
import { BottomTabNavigationEventMap } from '@react-navigation/bottom-tabs'
import { NavigationHelpers, ParamListBase } from '@react-navigation/native'
import { NavigationHelpers, ParamListBase, useNavigation } from '@react-navigation/native'
import { Text } from '../Global/helpers/text'
import TextTicker from 'react-native-text-ticker'
import PlayPauseButton from './components/buttons'
@@ -32,17 +32,17 @@ import Animated, {
withSpring,
} from 'react-native-reanimated'
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { RootStackParamList } from '../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
export const Miniplayer = React.memo(function Miniplayer({
navigation,
}: {
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>
}): React.JSX.Element {
export const Miniplayer = React.memo(function Miniplayer(): React.JSX.Element {
const { api } = useJellifyContext()
const nowPlaying = useNowPlayingContext()
const useSkip = useSkipContext()
const usePrevious = usePreviousContext()
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const translateX = useSharedValue(0)
const translateY = useSharedValue(0)
@@ -56,7 +56,7 @@ export const Miniplayer = React.memo(function Miniplayer({
useSkip()
} else if (direction === 'Swiped Up') {
// Navigate to the big player
navigation.navigate('Player')
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
}
},
[useSkip, usePrevious, navigation],
@@ -102,7 +102,9 @@ export const Miniplayer = React.memo(function Miniplayer({
margin={0}
padding={0}
height={'$7'}
onPress={() => navigation.navigate('Player')}
onPress={() =>
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
}
>
<YStack
justify='center'

View File

@@ -21,6 +21,9 @@ import Toast from 'react-native-toast-message'
import JellifyToastConfig from '../constants/toast.config'
import { useColorScheme } from 'react-native'
import { CarPlayProvider } from '../providers/CarPlay'
import { LibrarySortAndFilterProvider } from '../providers/Library/sorting-filtering'
import { LibraryProvider } from '../providers/Library'
import { HomeProvider } from '../providers/Home'
/**
* The main component for the Jellify app. Children are wrapped in the {@link JellifyProvider}
* @returns The {@link Jellify} component
@@ -83,8 +86,14 @@ function App(): React.JSX.Element {
<NetworkContextProvider>
<QueueProvider>
<PlayerProvider>
<CarPlayProvider />
<Root />
<HomeProvider>
<LibrarySortAndFilterProvider>
<LibraryProvider>
<CarPlayProvider />
<Root />
</LibraryProvider>
</LibrarySortAndFilterProvider>
</HomeProvider>
</PlayerProvider>
</QueueProvider>
</NetworkContextProvider>

View File

@@ -17,6 +17,7 @@ function AlbumContextInitializer(album: BaseItemDto): AlbumContext {
const { data: discs, isPending } = useQuery({
queryKey: [QueryKeys.ItemTracks, album.Id!],
queryFn: () => fetchAlbumDiscs(api, album),
enabled: true,
})
return {
@@ -41,7 +42,7 @@ export const AlbumProvider: ({
}) => React.JSX.Element = ({ album, children }) => {
const context = AlbumContextInitializer(album)
return <AlbumContext.Provider value={{ ...context }}>{children}</AlbumContext.Provider>
return <AlbumContext.Provider value={context}>{children}</AlbumContext.Provider>
}
export const useAlbumContext = () => useContext(AlbumContext)

View File

@@ -6,6 +6,8 @@ import { createContext, ReactNode, useCallback, useContext, useMemo } from 'reac
import { SharedValue, useSharedValue } from 'react-native-reanimated'
import { useJellifyContext } from '..'
import { fetchArtistAlbums, fetchArtistFeaturedOn } from '../../api/queries/artist'
import { isUndefined } from 'lodash'
import { Spinner } from 'tamagui'
interface ArtistContext {
fetchingAlbums: boolean
@@ -45,8 +47,9 @@ export const ArtistProvider = ({
refetch: refetchAlbums,
isPending: fetchingAlbums,
} = useQuery({
queryKey: [QueryKeys.ArtistAlbums, library?.musicLibraryId, artist.Id!],
queryKey: [QueryKeys.ArtistAlbums, library?.musicLibraryId, artist.Id],
queryFn: () => fetchArtistAlbums(api, library?.musicLibraryId, artist),
enabled: !isUndefined(artist.Id),
})
const {
@@ -54,8 +57,9 @@ export const ArtistProvider = ({
refetch: refetchFeaturedOn,
isPending: fetchingFeaturedOn,
} = useQuery({
queryKey: [QueryKeys.ArtistFeaturedOn, library?.musicLibraryId, artist.Id!],
queryKey: [QueryKeys.ArtistFeaturedOn, library?.musicLibraryId, artist.Id],
queryFn: () => fetchArtistFeaturedOn(api, library?.musicLibraryId, artist),
enabled: !isUndefined(artist.Id),
})
const {
@@ -63,8 +67,9 @@ export const ArtistProvider = ({
refetch: refetchSimilar,
isPending: fetchingSimilarArtists,
} = useQuery({
queryKey: [QueryKeys.SimilarItems, library?.musicLibraryId, artist.Id!],
queryKey: [QueryKeys.SimilarItems, library?.musicLibraryId, artist.Id],
queryFn: () => fetchSimilar(api, user, library?.musicLibraryId, artist.Id!),
enabled: !isUndefined(artist.Id),
})
const refresh = useCallback(() => {
@@ -100,7 +105,15 @@ export const ArtistProvider = ({
],
)
return <ArtistContext.Provider value={value}>{children}</ArtistContext.Provider>
return (
<ArtistContext.Provider value={value}>
{fetchingAlbums || fetchingFeaturedOn || fetchingSimilarArtists ? (
<Spinner color={'$primary'} flex={1} />
) : (
children
)}
</ArtistContext.Provider>
)
}
export const useArtistContext = () => useContext(ArtistContext)

View File

@@ -1,4 +1,11 @@
import React, { createContext, ReactNode, useContext, useState } from 'react'
import React, {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import {
InfiniteData,
@@ -12,6 +19,7 @@ import { queryClient } from '../../constants/query-client'
import QueryConfig from '../../api/queries/query.config'
import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from '../../api/queries/frequents'
import { useJellifyContext } from '..'
import { useIsFocused } from '@react-navigation/native'
interface HomeContext {
refreshing: boolean
onRefresh: () => void
@@ -36,6 +44,12 @@ const HomeContextInitializer = () => {
const { api, library, user } = useJellifyContext()
const [refreshing, setRefreshing] = useState<boolean>(false)
const isFocused = useIsFocused()
useEffect(() => {
console.debug(`Home focused: ${isFocused}`)
}, [isFocused])
const {
data: recentTracks,
isFetching: isFetchingRecentTracks,
@@ -98,7 +112,7 @@ const HomeContextInitializer = () => {
enabled: !!frequentlyPlayed && frequentlyPlayed.length > 0 && !isStaleFrequentlyPlayed,
})
const onRefresh = async () => {
const onRefresh = useCallback(async () => {
setRefreshing(true)
queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyPlayedArtists] })
@@ -114,7 +128,12 @@ const HomeContextInitializer = () => {
])
setRefreshing(false)
}
}, [
refetchRecentTracks,
refetchFrequentlyPlayed,
recentArtistsInfiniteQuery.refetch,
frequentArtistsInfiniteQuery.refetch,
])
return {
refreshing,

View File

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

View File

@@ -21,77 +21,75 @@ export default function Home(): React.JSX.Element {
const theme = useTheme()
return (
<HomeProvider>
<HomeStack.Navigator initialRouteName='Home' screenOptions={{ headerShown: true }}>
<HomeStack.Group>
<HomeStack.Screen
name='Home'
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.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>
</HomeProvider>
<HomeStack.Screen
name='Playlist'
component={PlaylistScreen}
options={({ route }) => ({
headerShown: true,
headerTitleStyle: {
color: theme.background.val,
},
})}
/>
</HomeStack.Group>
</HomeStack.Navigator>
)
}

View File

@@ -2,8 +2,11 @@ import { BaseStackParamList } from '../types'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { UseInfiniteQueryResult } from '@tanstack/react-query'
import { NavigatorScreenParams } from '@react-navigation/native'
type HomeStackParamList = BaseStackParamList & {
HomeScreen: undefined
RecentArtists: {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import Library from '../../components/Library/component'
import React from 'react'
import LibraryScreen from '../../components/Library/component'
import { PlaylistScreen } from '../Playlist'
import AddPlaylist from './add-playlist'
import DeletePlaylist from './delete-playlist'
@@ -9,121 +9,87 @@ import { LibraryProvider } from '../../providers/Library'
import { LibrarySortAndFilterProvider } from '../../providers/Library/sorting-filtering'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import AlbumScreen from '../Album'
import LibraryStackParamList, { LibraryNavigation } from './types'
import LibraryStackParamList from './types'
import { LibraryTabProps } from '../Tabs/types'
import { useIsFocused } from '@react-navigation/native'
import { LibraryNavigationContext } from './navigation'
const Stack = createNativeStackNavigator<LibraryStackParamList>()
export default function LibraryStack({ route, navigation }: LibraryTabProps): React.JSX.Element {
const theme = useTheme()
const isFocused = useIsFocused()
useEffect(() => {
if (!isFocused) return
if (LibraryNavigation.album) {
navigation.navigate('Library', {
screen: 'Album',
params: { album: LibraryNavigation.album },
})
LibraryNavigation.album = undefined
}
if (LibraryNavigation.artist) {
navigation.navigate('Library', {
screen: 'Artist',
params: { artist: LibraryNavigation.artist },
})
LibraryNavigation.artist = undefined
}
if (LibraryNavigation.playlist) {
navigation.navigate('Library', {
screen: 'Playlist',
params: { playlist: LibraryNavigation.playlist },
})
LibraryNavigation.playlist = undefined
}
}, [isFocused])
return (
<LibrarySortAndFilterProvider>
<LibraryProvider>
<Stack.Navigator initialRouteName='Library'>
<Stack.Screen
name='Library'
component={Library}
options={{
title: 'Library',
<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>
</LibraryProvider>
</LibrarySortAndFilterProvider>
<Stack.Screen
name='DeletePlaylist'
component={DeletePlaylist}
options={{
title: 'Delete Playlist',
}}
/>
</Stack.Group>
</Stack.Navigator>
)
}

View File

@@ -0,0 +1,16 @@
import { NavigationProp } from '@react-navigation/native'
import LibraryStackParamList from './types'
import { createContext, useContext } from 'use-context-selector'
import TabParamList from '../Tabs/types'
export const LibraryNavigationContext = createContext<NavigationProp<TabParamList> | null>(null)
const useLibraryNavigation = () => {
const context = useContext(LibraryNavigationContext)
if (!context)
throw new Error('useLibraryNavigation must be used in the the LibraryNavigationProvider')
return context
}
export default useLibraryNavigation

View File

@@ -1,8 +1,11 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../types'
import { Queue } from '../../player/types/queue-item'
import { NavigatorScreenParams } from '@react-navigation/native'
type LibraryStackParamList = BaseStackParamList & {
LibraryScreen: NavigatorScreenParams<BaseStackParamList>
AddPlaylist: undefined
DeletePlaylist: {
@@ -12,7 +15,7 @@ type LibraryStackParamList = BaseStackParamList & {
export default LibraryStackParamList
export type LibraryScreenProps = NativeStackScreenProps<LibraryStackParamList, 'Library'>
export type LibraryScreenProps = NativeStackScreenProps<LibraryScreenParamList, 'LibraryScreen'>
export type LibraryArtistProps = NativeStackScreenProps<LibraryStackParamList, 'Artist'>
export type LibraryAlbumProps = NativeStackScreenProps<LibraryStackParamList, 'Album'>
@@ -22,10 +25,6 @@ export type LibraryDeletePlaylistProps = NativeStackScreenProps<
'DeletePlaylist'
>
type LibraryNavigation = {
album?: BaseItemDto
artist?: BaseItemDto
playlist?: BaseItemDto
type LibraryScreenParamList = {
LibraryScreen: NavigatorScreenParams<LibraryStackParamList>
}
export const LibraryNavigation: LibraryNavigation = {}

View File

@@ -16,11 +16,12 @@ import Toast from 'react-native-toast-message'
import { IS_MAESTRO_BUILD } from '../../configs/config'
import { AxiosResponse } from 'axios'
import { AuthenticationResult } from '@jellyfin/sdk/lib/generated-client/models'
import LoginStackParamList from './types'
export default function ServerAuthentication({
navigation,
}: {
navigation: NativeStackNavigationProp<RootStackParamList>
navigation: NativeStackNavigationProp<LoginStackParamList>
}): React.JSX.Element {
const { api } = useJellifyContext()
const [username, setUsername] = useState<string | undefined>(undefined)
@@ -116,10 +117,7 @@ export default function ServerAuthentication({
bordered={0}
onPress={() => {
if (navigation.canGoBack()) navigation.goBack()
else
navigation.navigate('ServerAddress', undefined, {
pop: true,
})
else navigation.navigate('ServerAddress', undefined, { pop: true })
}}
>
Switch Server

View File

@@ -0,0 +1,7 @@
type LoginStackParamList = {
ServerAddress: undefined
ServerAuthentication: undefined
LibrarySelection: undefined
}
export default LoginStackParamList

View File

@@ -9,9 +9,9 @@ export const PlayerStack = createNativeStackNavigator<PlayerParamList>()
export default function Player(): React.JSX.Element {
return (
<PlayerStack.Navigator initialRouteName='Player'>
<PlayerStack.Navigator initialRouteName='PlayerScreen'>
<PlayerStack.Screen
name='Player'
name='PlayerScreen'
component={PlayerScreen}
options={{
headerShown: false,
@@ -20,7 +20,7 @@ export default function Player(): React.JSX.Element {
/>
<PlayerStack.Screen
name='Queue'
name='QueueScreen'
component={Queue}
options={{
headerTitle: '',
@@ -28,7 +28,7 @@ export default function Player(): React.JSX.Element {
/>
<PlayerStack.Screen
name='MultipleArtists'
name='MultipleArtistsSheet'
component={MultipleArtistsSheet}
options={{
presentation: 'formSheet',

View File

@@ -2,13 +2,14 @@ import { RootStackParamList } from '../types'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
type PlayerParamList = RootStackParamList & {
Player: undefined
Queue: undefined
type PlayerParamList = {
PlayerScreen: undefined
QueueScreen: undefined
MultipleArtists: {
MultipleArtistsSheet: {
artists: BaseItemDto[]
}
}
export type MultipleArtistsProps = NativeStackScreenProps<PlayerParamList, 'MultipleArtists'>
export type PlayerProps = NativeStackScreenProps<PlayerParamList, 'PlayerScreen'>
export type MultipleArtistsProps = NativeStackScreenProps<PlayerParamList, 'MultipleArtistsSheet'>

View File

@@ -11,16 +11,17 @@ import SearchStack from '../Search'
import LibraryStack from '../Library'
import InternetConnectionWatcher from '../../components/Network/internetConnectionWatcher'
import TabParamList from './types'
import { TabProps } from '../types'
const Tab = createBottomTabNavigator<TabParamList>()
export function Tabs(): React.JSX.Element {
export default function Tabs({ route, navigation }: TabProps): React.JSX.Element {
const theme = useTheme()
const nowPlaying = useNowPlayingContext()
return (
<Tab.Navigator
initialRouteName='Home'
initialRouteName={route.params?.screen ?? 'HomeTab'}
screenOptions={{
animation: 'shift',
tabBarActiveTintColor: theme.primary.val,
@@ -30,7 +31,7 @@ export function Tabs(): React.JSX.Element {
<>
{nowPlaying && (
/* Hide miniplayer if the queue is empty */
<Miniplayer navigation={props.navigation} />
<Miniplayer />
)}
<InternetConnectionWatcher />
@@ -39,9 +40,10 @@ export function Tabs(): React.JSX.Element {
)}
>
<Tab.Screen
name='Home'
name='HomeTab'
component={Home}
options={{
title: 'Home',
headerShown: false,
tabBarIcon: ({ color, size }) => (
<MaterialDesignIcons name='jellyfish-outline' color={color} size={size} />
@@ -51,21 +53,24 @@ export function Tabs(): React.JSX.Element {
/>
<Tab.Screen
name='Library'
name='LibraryTab'
component={LibraryStack}
options={{
title: 'Library',
headerShown: false,
tabBarIcon: ({ color, size }) => (
<MaterialDesignIcons name='music-box-multiple' color={color} size={size} />
),
tabBarButtonTestID: 'library-tab-button',
lazy: false, // Load on mount since we need to be able to navigate here from the player
}}
/>
<Tab.Screen
name='Search'
name='SearchTab'
component={SearchStack}
options={{
title: 'Search',
headerShown: false,
tabBarIcon: ({ color, size }) => (
<MaterialDesignIcons name='magnify' color={color} size={size} />
@@ -75,9 +80,10 @@ export function Tabs(): React.JSX.Element {
/>
<Tab.Screen
name='Discover'
name='DiscoverTab'
component={Discover}
options={{
title: 'Discover',
headerShown: false,
tabBarIcon: ({ color, size }) => (
<MaterialDesignIcons name='earth' color={color} size={size} />
@@ -87,9 +93,10 @@ export function Tabs(): React.JSX.Element {
/>
<Tab.Screen
name='Settings'
name='SettingsTab'
component={SettingsScreen}
options={{
title: 'Settings',
headerShown: false,
tabBarIcon: ({ color, size }) => (
<MaterialDesignIcons name='dip-switch' color={color} size={size} />

View File

@@ -3,14 +3,14 @@ import { NavigatorScreenParams } from '@react-navigation/native'
import LibraryStackParamList from '../Library/types'
type TabParamList = {
Home: undefined
Library: NavigatorScreenParams<LibraryStackParamList>
Search: undefined
Discover: undefined
Settings: undefined
HomeTab: undefined
LibraryTab: NavigatorScreenParams<LibraryStackParamList>
SearchTab: undefined
DiscoverTab: undefined
SettingsTab: undefined
}
export type HomeTabProps = BottomTabScreenProps<TabParamList, 'Home'>
export type LibraryTabProps = BottomTabScreenProps<TabParamList, 'Library'>
export type HomeTabProps = BottomTabScreenProps<TabParamList, 'HomeTab'>
export type LibraryTabProps = BottomTabScreenProps<TabParamList, 'LibraryTab'>
export default TabParamList

View File

@@ -1,5 +1,5 @@
import Player from './Player'
import { Tabs } from './Tabs'
import Tabs from './Tabs'
import { RootStackParamList } from './types'
import { getToken, useTheme } from 'tamagui'
import { useJellifyContext } from '../providers'
@@ -20,7 +20,7 @@ export default function Root(): React.JSX.Element {
initialRouteName={api && library ? 'Tabs' : 'Login'}
screenOptions={({ route }) => ({
navigationBarColor:
route.name === 'Player' ? getToken('$black') : theme.background.val,
route.name === 'PlayerRoot' ? getToken('$black') : theme.background.val,
})}
>
<RootStack.Screen
@@ -33,7 +33,7 @@ export default function Root(): React.JSX.Element {
}}
/>
<RootStack.Screen
name='Player'
name='PlayerRoot'
component={Player}
options={{
presentation: 'modal',

View File

@@ -17,12 +17,6 @@ import TabParamList from './Tabs/types'
import { PlayerParamList } from './Player/types'
export type BaseStackParamList = {
Home: undefined
Library: undefined
Search: undefined
Discover: undefined
Settings: undefined
Artist: {
artist: BaseItemDto
}
@@ -52,6 +46,7 @@ export type BaseStackParamList = {
export type ArtistProps = NativeStackScreenProps<BaseStackParamList, 'Artist'>
export type AlbumProps = NativeStackScreenProps<BaseStackParamList, 'Album'>
export type PlaylistProps = NativeStackNavigationProp<BaseStackParamList, 'Playlist'>
export type TracksProps = NativeStackScreenProps<BaseStackParamList, 'Tracks'>
export type InstantMixProps = NativeStackScreenProps<BaseStackParamList, 'InstantMix'>
@@ -59,19 +54,20 @@ export type RootStackParamList = {
Login: undefined
Tabs: NavigatorScreenParams<TabParamList>
Player: NavigatorScreenParams<PlayerParamList>
PlayerRoot: NavigatorScreenParams<PlayerParamList>
Context: {
item: BaseItemDto
navigation?: NativeStackNavigationProp<
HomeStackParamList | LibraryStackParamList | DiscoverStackParamList
>
navigationCallback?: (screen: 'Album' | 'Artist', item: BaseItemDto) => void
}
}
export type LoginProps = NativeStackNavigationProp<RootStackParamList, 'Login'>
export type TabProps = NativeStackScreenProps<RootStackParamList, 'Tabs'>
export type PlayerProps = NativeStackScreenProps<RootStackParamList, 'Player'>
export type PlayerProps = NativeStackScreenProps<RootStackParamList, 'PlayerRoot'>
export type ContextProps = NativeStackScreenProps<RootStackParamList, 'Context'>
export type ArtistsProps = {