mirror of
https://github.com/Jellify-Music/App.git
synced 2026-02-20 18:58:31 -06:00
context sheet prefetching
unit tests update rn screens
This commit is contained in:
3
App.tsx
3
App.tsx
@@ -24,6 +24,7 @@ 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 { PROGRESS_UPDATE_EVENT_INTERVAL } from './src/player/config'
|
||||
|
||||
export default function App(): React.JSX.Element {
|
||||
// Add performance monitoring to track app-level re-renders
|
||||
@@ -59,7 +60,7 @@ export default function App(): React.JSX.Element {
|
||||
capabilities: CAPABILITIES,
|
||||
notificationCapabilities: CAPABILITIES,
|
||||
// Reduced interval for smoother progress tracking and earlier prefetch detection
|
||||
progressUpdateEventInterval: 5,
|
||||
progressUpdateEventInterval: PROGRESS_UPDATE_EVENT_INTERVAL,
|
||||
}),
|
||||
)
|
||||
.finally(() => {
|
||||
|
||||
@@ -2719,7 +2719,7 @@ PODS:
|
||||
- RNWorklets
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens (4.14.1):
|
||||
- RNScreens (4.15.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -2746,10 +2746,10 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- RNScreens/common (= 4.14.1)
|
||||
- RNScreens/common (= 4.15.0)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- RNScreens/common (4.14.1):
|
||||
- RNScreens/common (4.15.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -3306,7 +3306,7 @@ SPEC CHECKSUMS:
|
||||
RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961
|
||||
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
|
||||
RNReanimated: ee96d03fe3713993a30cc205522792b4cb08e4f9
|
||||
RNScreens: 6ced6ae8a526512a6eef6e28c2286e1fc2d378c3
|
||||
RNScreens: 48bbaca97a5f9aedc3e52bd48673efd2b6aac4f6
|
||||
RNSentry: 95e1ed0ede28a4af58aaafedeac9fcfaba0e89ce
|
||||
RNWorklets: e8335dff9d27004709f58316985769040cd1e8f2
|
||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||
|
||||
28
jest/functional/Player-Handlers.test.ts
Normal file
28
jest/functional/Player-Handlers.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Progress } from 'react-native-track-player'
|
||||
import { shouldMarkPlaybackFinished } from '../../src/providers/Player/utils/handlers'
|
||||
|
||||
describe('Playback Event Handlers', () => {
|
||||
it('should determine that the track has finished', () => {
|
||||
const progress: Progress = {
|
||||
position: 95.23423453,
|
||||
duration: 98.23557854,
|
||||
buffered: 98.2345568679345,
|
||||
}
|
||||
|
||||
const playbackFinished = shouldMarkPlaybackFinished(progress)
|
||||
|
||||
expect(playbackFinished).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should determine the track is still playing', () => {
|
||||
const progress: Progress = {
|
||||
position: 85.23423453,
|
||||
duration: 98.23557854,
|
||||
buffered: 98.2345568679345,
|
||||
}
|
||||
|
||||
const playbackFinished = shouldMarkPlaybackFinished(progress)
|
||||
|
||||
expect(playbackFinished).toBeFalsy()
|
||||
})
|
||||
})
|
||||
@@ -82,7 +82,7 @@
|
||||
"react-native-pager-view": "^7.0.0",
|
||||
"react-native-reanimated": "4.0.2",
|
||||
"react-native-safe-area-context": "^5.6.0",
|
||||
"react-native-screens": "4.14.1",
|
||||
"react-native-screens": "4.15.0",
|
||||
"react-native-swipeable-item": "^2.0.9",
|
||||
"react-native-text-ticker": "^1.15.0",
|
||||
"react-native-toast-message": "^2.3.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { BaseItemDto, PlaybackInfoResponse } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getAudioApi, getMediaInfoApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { PlaybackInfoResponse } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getMediaInfoApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
import { AudioQuality } from '../../types/AudioQuality'
|
||||
@@ -9,7 +9,7 @@ export async function fetchMediaInfo(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
bitrate: AudioQuality | undefined,
|
||||
item: BaseItemDto,
|
||||
itemId: string,
|
||||
): Promise<PlaybackInfoResponse> {
|
||||
console.debug(`Fetching media info of quality ${JSON.stringify(bitrate)}`)
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function fetchMediaInfo(
|
||||
|
||||
getMediaInfoApi(api)
|
||||
.getPostedPlaybackInfo({
|
||||
itemId: item.Id!,
|
||||
itemId: itemId!,
|
||||
userId: user.id,
|
||||
playbackInfoDto: {
|
||||
MaxAudioChannels: bitrate?.MaxAudioBitDepth
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { BaseItemDto, BaseItemKind, ItemFields } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { isUndefined } from 'lodash'
|
||||
@@ -54,14 +54,12 @@ export async function fetchArtistSuggestions(
|
||||
if (isUndefined(user)) return reject('User has not been set')
|
||||
if (isUndefined(libraryId)) return reject('Library has not been set')
|
||||
|
||||
getItemsApi(api)
|
||||
.getItems({
|
||||
getArtistsApi(api)
|
||||
.getAlbumArtists({
|
||||
parentId: libraryId,
|
||||
userId: user.id,
|
||||
recursive: true,
|
||||
limit: 50,
|
||||
startIndex: page * 50,
|
||||
includeItemTypes: [BaseItemKind.MusicArtist],
|
||||
fields: [ItemFields.ChildCount, ItemFields.SortName],
|
||||
sortBy: ['Random'],
|
||||
})
|
||||
|
||||
@@ -120,11 +120,13 @@ export default function AddToPlaylist({ track }: { track: BaseItemDto }): React.
|
||||
return (
|
||||
<ScrollView>
|
||||
<XStack gap={'$2'} margin={'$4'}>
|
||||
<ItemImage item={track} />
|
||||
<ItemImage item={track} width={'$12'} height={'$12'} />
|
||||
|
||||
<YStack gap={'$2'} margin={'$2'}>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold>{getItemName(track)}</Text>
|
||||
<Text bold fontSize={'$6'}>
|
||||
{getItemName(track)}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
|
||||
@@ -79,6 +79,14 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re
|
||||
|
||||
const renderViewAlbumRow = useMemo(() => isAlbum || (isTrack && album), [album, item])
|
||||
|
||||
const artistIds = !isPlaylist
|
||||
? isArtist
|
||||
? [item.Id]
|
||||
: item.ArtistItems
|
||||
? item.ArtistItems.map((item) => item.Id)
|
||||
: []
|
||||
: []
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<YGroup unstyled marginBottom={bottom}>
|
||||
@@ -108,10 +116,7 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re
|
||||
)}
|
||||
|
||||
{!isPlaylist && (
|
||||
<ViewArtistMenuRow
|
||||
artists={isArtist ? [item] : itemArtists}
|
||||
stackNavigation={stackNavigation}
|
||||
/>
|
||||
<ArtistMenuRows artistIds={artistIds} stackNavigation={stackNavigation} />
|
||||
)}
|
||||
</YGroup>
|
||||
</ScrollView>
|
||||
@@ -210,13 +215,37 @@ function ViewAlbumMenuRow({ album: album, stackNavigation }: MenuRowProps): Reac
|
||||
)
|
||||
}
|
||||
|
||||
function ViewArtistMenuRow({
|
||||
artists,
|
||||
function ArtistMenuRows({
|
||||
artistIds,
|
||||
stackNavigation,
|
||||
}: {
|
||||
artists: BaseItemDto[]
|
||||
artistIds: (string | null | undefined)[]
|
||||
stackNavigation: StackNavigation | undefined
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<View>
|
||||
{artistIds.map((id) => (
|
||||
<ViewArtistMenuRow artistId={id} key={id} stackNavigation={stackNavigation} />
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ViewArtistMenuRow({
|
||||
artistId,
|
||||
stackNavigation,
|
||||
}: {
|
||||
artistId: string | null | undefined
|
||||
stackNavigation: StackNavigation | undefined
|
||||
}): React.JSX.Element {
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
const { data: artist } = useQuery({
|
||||
queryKey: [QueryKeys.ArtistById, artistId],
|
||||
queryFn: () => fetchItem(api, artistId!),
|
||||
enabled: !!artistId,
|
||||
})
|
||||
|
||||
const goToArtist = useCallback(
|
||||
(artist: BaseItemDto) => {
|
||||
if (stackNavigation) stackNavigation.navigate('Artist', { artist })
|
||||
@@ -225,23 +254,20 @@ function ViewArtistMenuRow({
|
||||
[stackNavigation, navigationRef],
|
||||
)
|
||||
|
||||
return (
|
||||
<View>
|
||||
{artists.map((artist, index) => (
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
backgroundColor={'transparent'}
|
||||
gap={'$3'}
|
||||
justifyContent='flex-start'
|
||||
key={index}
|
||||
onPress={() => goToArtist(artist)}
|
||||
pressStyle={{ opacity: 0.5 }}
|
||||
>
|
||||
<ItemImage circular item={artist} height={'$10'} width={'$10'} />
|
||||
return artist ? (
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
backgroundColor={'transparent'}
|
||||
gap={'$3'}
|
||||
justifyContent='flex-start'
|
||||
onPress={() => goToArtist(artist)}
|
||||
pressStyle={{ opacity: 0.5 }}
|
||||
>
|
||||
<ItemImage circular item={artist} height={'$10'} width={'$10'} />
|
||||
|
||||
<Text bold>{`Go to ${getItemName(artist)}`}</Text>
|
||||
</ListItem>
|
||||
))}
|
||||
</View>
|
||||
<Text bold>{`Go to ${getItemName(artist)}`}</Text>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,17 @@ 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'))
|
||||
// Pop Context Sheet
|
||||
navigationRef.dispatch(StackActions.pop())
|
||||
|
||||
const route = navigationRef.current?.getCurrentRoute()
|
||||
let route = navigationRef.current?.getCurrentRoute()
|
||||
|
||||
// If we've popped into the player, pop that as well
|
||||
if (route?.name.includes('Player')) {
|
||||
navigationRef.dispatch(StackActions.pop())
|
||||
|
||||
route = navigationRef.current?.getCurrentRoute()
|
||||
}
|
||||
|
||||
if (route?.name.includes('Settings')) {
|
||||
navigationRef.dispatch(TabActions.jumpTo('LibraryTab'))
|
||||
@@ -22,10 +29,17 @@ export function goToAlbumFromContextSheet(album: BaseItemDto | undefined) {
|
||||
export function goToArtistFromContextSheet(artist: BaseItemDto | undefined) {
|
||||
if (!navigationRef.isReady() || !artist) return
|
||||
|
||||
// Pop Context Sheet and Player Modal
|
||||
navigationRef.dispatch(StackActions.popTo('Tabs'))
|
||||
// Pop Context Sheet
|
||||
navigationRef.dispatch(StackActions.pop())
|
||||
|
||||
const route = navigationRef.current?.getCurrentRoute()
|
||||
let route = navigationRef.current?.getCurrentRoute()
|
||||
|
||||
// If we've popped into the player, pop that as well
|
||||
if (route?.name.includes('Player')) {
|
||||
navigationRef.dispatch(StackActions.pop())
|
||||
|
||||
route = navigationRef.current?.getCurrentRoute()
|
||||
}
|
||||
|
||||
if (route?.name.includes('Settings')) {
|
||||
navigationRef.dispatch(TabActions.jumpTo('LibraryTab'))
|
||||
|
||||
@@ -83,10 +83,7 @@ function getBorderRadius(circular: boolean | undefined, width: Token | number |
|
||||
if (circular) {
|
||||
borderRadius = width ? (typeof width === 'number' ? width : getTokenValue(width)) : '100%'
|
||||
} else if (!isUndefined(width)) {
|
||||
borderRadius =
|
||||
typeof width === 'number'
|
||||
? width / 25
|
||||
: getTokenValue(width) / (getTokenValue(width) / 6)
|
||||
borderRadius = typeof width === 'number' ? width / 25 : getTokenValue(width) / 15
|
||||
} else borderRadius = '5%'
|
||||
|
||||
return borderRadius
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import React from 'react'
|
||||
import { CardProps as TamaguiCardProps } from 'tamagui'
|
||||
import { getToken, Card as TamaguiCard, View, YStack } from 'tamagui'
|
||||
import { BaseItemDto, BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Text } from '../helpers/text'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { getQualityParams } from '../../../utils/mappings'
|
||||
import { useStreamingQualityContext } from '../../../providers/Settings'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../../../api/queries/item'
|
||||
import { ItemProvider } from '../../../providers/Item'
|
||||
import ItemImage from './image'
|
||||
|
||||
interface CardProps extends TamaguiCardProps {
|
||||
caption?: string | null | undefined
|
||||
@@ -29,119 +22,56 @@ interface CardProps extends TamaguiCardProps {
|
||||
* @param props
|
||||
*/
|
||||
export function ItemCard(props: CardProps) {
|
||||
const { api, user } = useJellifyContext()
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, props.item.Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), props.item),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: props.item.Type === BaseItemKind.Audio,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire query for a track's album
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.Album, props.item.AlbumId],
|
||||
queryFn: () => fetchItem(api, props.item.AlbumId!),
|
||||
enabled: props.item.Type === BaseItemKind.Audio && !!props.item.AlbumId,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire query for an album's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, props.item.Id],
|
||||
queryFn: () => fetchAlbumDiscs(api, props.item),
|
||||
enabled: !!props.item.Id && props.item.Type === BaseItemKind.MusicAlbum,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire query for an playlist's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, props.item.Id],
|
||||
queryFn: () =>
|
||||
getItemsApi(api!)
|
||||
.getItems({ parentId: props.item.Id! })
|
||||
.then(({ data }) => {
|
||||
if (data.Items) return data.Items
|
||||
else return []
|
||||
}),
|
||||
enabled: !!props.item.Id && props.item.Type === BaseItemKind.Playlist,
|
||||
})
|
||||
|
||||
return (
|
||||
<View alignItems='center' margin={'$1.5'}>
|
||||
<TamaguiCard
|
||||
size={'$12'}
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
testID={props.testId ?? undefined}
|
||||
backgroundColor={getToken('$color.amethyst')}
|
||||
circular={!props.squared}
|
||||
borderRadius={props.squared ? 5 : 'unset'}
|
||||
animation='bouncy'
|
||||
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
|
||||
pressStyle={props.onPress ? { scale: 0.875 } : {}}
|
||||
{...props}
|
||||
>
|
||||
<TamaguiCard.Header></TamaguiCard.Header>
|
||||
<TamaguiCard.Footer padded>
|
||||
{/* { props.item.Type === 'MusicArtist' && (
|
||||
<BlurhashedImage
|
||||
cornered
|
||||
item={props.item}
|
||||
type={ImageType.Logo}
|
||||
width={logoDimensions.width}
|
||||
height={logoDimensions.height}
|
||||
/>
|
||||
)} */}
|
||||
</TamaguiCard.Footer>
|
||||
<TamaguiCard.Background>
|
||||
<FastImage
|
||||
source={{
|
||||
uri:
|
||||
getImageApi(api!).getItemImageUrlById(
|
||||
props.item.Type === 'Audio'
|
||||
? props.item.AlbumId! || props.item.Id!
|
||||
: props.item.Id!,
|
||||
ImageType.Primary,
|
||||
{
|
||||
tag: props.item.AlbumId
|
||||
? props.item.AlbumPrimaryImageTag!
|
||||
: props.item.ImageTags?.Primary,
|
||||
},
|
||||
) || '',
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: props.squared ? 2 : 100,
|
||||
}}
|
||||
/>
|
||||
</TamaguiCard.Background>
|
||||
</TamaguiCard>
|
||||
{props.caption && (
|
||||
<YStack alignContent='center' alignItems='center' maxWidth={props.size}>
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{props.caption}
|
||||
</Text>
|
||||
|
||||
{props.subCaption && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1} textAlign='center'>
|
||||
{props.subCaption}
|
||||
<ItemProvider item={props.item}>
|
||||
<View alignItems='center' margin={'$1.5'}>
|
||||
<TamaguiCard
|
||||
size={'$12'}
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
testID={props.testId ?? undefined}
|
||||
backgroundColor={getToken('$color.amethyst')}
|
||||
circular={!props.squared}
|
||||
borderRadius={props.squared ? '$5' : 'unset'}
|
||||
animation='bouncy'
|
||||
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
|
||||
pressStyle={props.onPress ? { scale: 0.875 } : {}}
|
||||
{...props}
|
||||
>
|
||||
<TamaguiCard.Header></TamaguiCard.Header>
|
||||
<TamaguiCard.Footer padded>
|
||||
{/* { props.item.Type === 'MusicArtist' && (
|
||||
<BlurhashedImage
|
||||
cornered
|
||||
item={props.item}
|
||||
type={ImageType.Logo}
|
||||
width={logoDimensions.width}
|
||||
height={logoDimensions.height}
|
||||
/>
|
||||
)} */}
|
||||
</TamaguiCard.Footer>
|
||||
<TamaguiCard.Background>
|
||||
<ItemImage item={props.item} circular={!props.squared} />
|
||||
</TamaguiCard.Background>
|
||||
</TamaguiCard>
|
||||
{props.caption && (
|
||||
<YStack alignContent='center' alignItems='center' maxWidth={props.size}>
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{props.caption}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{props.subCaption && (
|
||||
<Text
|
||||
lineBreakStrategyIOS='standard'
|
||||
numberOfLines={1}
|
||||
textAlign='center'
|
||||
>
|
||||
{props.subCaption}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
)}
|
||||
</View>
|
||||
</ItemProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import Icon from './icon'
|
||||
@@ -9,17 +9,10 @@ import ItemImage from './image'
|
||||
import FavoriteIcon from './favorite-icon'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { runOnJS } from 'react-native-reanimated'
|
||||
import { getQualityParams } from '../../../utils/mappings'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { useStreamingQualityContext } from '../../../providers/Settings'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '../../../screens/types'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../../../api/queries/item'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { ItemProvider } from '../../../providers/Item'
|
||||
|
||||
interface ItemRowProps {
|
||||
item: BaseItemDto
|
||||
@@ -43,64 +36,10 @@ interface ItemRowProps {
|
||||
export default function ItemRow({
|
||||
item,
|
||||
navigation,
|
||||
queueName,
|
||||
onPress,
|
||||
circular,
|
||||
}: ItemRowProps): React.JSX.Element {
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
const { api, user } = useJellifyContext()
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
/**
|
||||
* Fire a query for fetching a track's media sources
|
||||
*
|
||||
* Referenced later when queuing tracks
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, item.Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), item),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: item.Type === 'Audio',
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire a query for fetching an album for a given track
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.Album, item.AlbumId],
|
||||
queryFn: () => fetchItem(api, item.AlbumId!),
|
||||
enabled: item.Type === BaseItemKind.Audio && !!item.AlbumId,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire a query for fetching an album's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, item.Id],
|
||||
queryFn: () => fetchAlbumDiscs(api, item),
|
||||
enabled: !!item.Id && item.Type === BaseItemKind.MusicAlbum,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire query for an playlist's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, item.Id],
|
||||
queryFn: () =>
|
||||
getItemsApi(api!)
|
||||
.getItems({ parentId: item.Id! })
|
||||
.then(({ data }) => {
|
||||
if (data.Items) return data.Items
|
||||
else return []
|
||||
}),
|
||||
enabled: !!item.Id && item.Type === BaseItemKind.Playlist,
|
||||
})
|
||||
|
||||
const gestureCallback = () => {
|
||||
switch (item.Type) {
|
||||
@@ -127,97 +66,99 @@ export default function ItemRow({
|
||||
})
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={gesture}>
|
||||
<XStack
|
||||
alignContent='center'
|
||||
minHeight={'$7'}
|
||||
width={'100%'}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', {
|
||||
item,
|
||||
navigation,
|
||||
})
|
||||
}}
|
||||
onPress={() => {
|
||||
if (onPress) {
|
||||
onPress()
|
||||
return
|
||||
}
|
||||
|
||||
switch (item.Type) {
|
||||
case 'MusicArtist': {
|
||||
navigation?.navigate('Artist', { artist: item })
|
||||
break
|
||||
}
|
||||
|
||||
case 'MusicAlbum': {
|
||||
navigation?.navigate('Album', { album: item })
|
||||
break
|
||||
}
|
||||
}
|
||||
}}
|
||||
paddingVertical={'$2'}
|
||||
paddingRight={'$2'}
|
||||
>
|
||||
<YStack marginHorizontal={'$3'} justifyContent='center'>
|
||||
<ItemImage
|
||||
item={item}
|
||||
height={'$12'}
|
||||
width={'$12'}
|
||||
circular={item.Type === 'MusicArtist' || circular}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<YStack alignContent='center' justifyContent='center' flex={4}>
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.Name ?? ''}
|
||||
</Text>
|
||||
{item.Type === 'MusicArtist' && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{`${item.ChildCount ?? 0} ${item.ChildCount === 1 ? 'Album' : 'Albums'}`}
|
||||
</Text>
|
||||
)}
|
||||
{(item.Type === 'Audio' || item.Type === 'MusicAlbum') && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.AlbumArtist ?? 'Untitled Artist'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{item.Type === 'Playlist' && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.Genres?.join(', ') ?? ''}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
<ItemProvider item={item}>
|
||||
<GestureDetector gesture={gesture}>
|
||||
<XStack
|
||||
justifyContent='flex-end'
|
||||
alignItems='center'
|
||||
flex={['Audio', 'MusicAlbum'].includes(item.Type ?? '') ? 2 : 1}
|
||||
>
|
||||
<FavoriteIcon item={item} />
|
||||
{/* Runtime ticks for Songs */}
|
||||
{['Audio', 'MusicAlbum'].includes(item.Type ?? '') ? (
|
||||
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
||||
) : ['Playlist'].includes(item.Type ?? '') ? (
|
||||
<Text
|
||||
color={'$borderColor'}
|
||||
>{`${item.ChildCount ?? 0} ${item.ChildCount === 1 ? 'Track' : 'Tracks'}`}</Text>
|
||||
) : null}
|
||||
alignContent='center'
|
||||
minHeight={'$7'}
|
||||
width={'100%'}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', {
|
||||
item,
|
||||
navigation,
|
||||
})
|
||||
}}
|
||||
onPress={() => {
|
||||
if (onPress) {
|
||||
onPress()
|
||||
return
|
||||
}
|
||||
|
||||
{item.Type === 'Audio' || item.Type === 'MusicAlbum' ? (
|
||||
<Icon
|
||||
name='dots-horizontal'
|
||||
onPress={() => {
|
||||
navigationRef.navigate('Context', {
|
||||
item,
|
||||
navigation,
|
||||
})
|
||||
}}
|
||||
switch (item.Type) {
|
||||
case 'MusicArtist': {
|
||||
navigation?.navigate('Artist', { artist: item })
|
||||
break
|
||||
}
|
||||
|
||||
case 'MusicAlbum': {
|
||||
navigation?.navigate('Album', { album: item })
|
||||
break
|
||||
}
|
||||
}
|
||||
}}
|
||||
paddingVertical={'$2'}
|
||||
paddingRight={'$2'}
|
||||
>
|
||||
<YStack marginHorizontal={'$3'} justifyContent='center'>
|
||||
<ItemImage
|
||||
item={item}
|
||||
height={'$12'}
|
||||
width={'$12'}
|
||||
circular={item.Type === 'MusicArtist' || circular}
|
||||
/>
|
||||
) : null}
|
||||
</YStack>
|
||||
|
||||
<YStack alignContent='center' justifyContent='center' flex={4}>
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.Name ?? ''}
|
||||
</Text>
|
||||
{item.Type === 'MusicArtist' && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{`${item.ChildCount ?? 0} ${item.ChildCount === 1 ? 'Album' : 'Albums'}`}
|
||||
</Text>
|
||||
)}
|
||||
{(item.Type === 'Audio' || item.Type === 'MusicAlbum') && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.AlbumArtist ?? 'Untitled Artist'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{item.Type === 'Playlist' && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.Genres?.join(', ') ?? ''}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
<XStack
|
||||
justifyContent='flex-end'
|
||||
alignItems='center'
|
||||
flex={['Audio', 'MusicAlbum'].includes(item.Type ?? '') ? 2 : 1}
|
||||
>
|
||||
<FavoriteIcon item={item} />
|
||||
{/* Runtime ticks for Songs */}
|
||||
{['Audio', 'MusicAlbum'].includes(item.Type ?? '') ? (
|
||||
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
||||
) : ['Playlist'].includes(item.Type ?? '') ? (
|
||||
<Text
|
||||
color={'$borderColor'}
|
||||
>{`${item.ChildCount ?? 0} ${item.ChildCount === 1 ? 'Track' : 'Tracks'}`}</Text>
|
||||
) : null}
|
||||
|
||||
{item.Type === 'Audio' || item.Type === 'MusicAlbum' ? (
|
||||
<Icon
|
||||
name='dots-horizontal'
|
||||
onPress={() => {
|
||||
navigationRef.navigate('Context', {
|
||||
item,
|
||||
navigation,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
</GestureDetector>
|
||||
</GestureDetector>
|
||||
</ItemProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ export default function Track({
|
||||
} else {
|
||||
navigationRef.navigate('Context', {
|
||||
item: track,
|
||||
navigation,
|
||||
})
|
||||
}
|
||||
}, [onLongPress, track, isNested])
|
||||
@@ -144,7 +145,7 @@ export default function Track({
|
||||
// Only fetch media info if needed (for streaming)
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, track.Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track),
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track.Id!),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: !isDownloaded, // Only fetch if not downloaded
|
||||
})
|
||||
@@ -152,7 +153,7 @@ export default function Track({
|
||||
// Fire query for fetching the track's media sources
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, track.Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track),
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track.Id!),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: track.Type === 'Audio',
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ function BlurredBackground({
|
||||
const gradientStyle2 = {
|
||||
width,
|
||||
height,
|
||||
flex: 2,
|
||||
flex: 3,
|
||||
}
|
||||
|
||||
const backgroundStyle = {
|
||||
|
||||
@@ -49,18 +49,19 @@ export default function PlayerHeader(): React.JSX.Element {
|
||||
<Spacer flex={1} />
|
||||
</XStack>
|
||||
|
||||
<YStack flexGrow={1} justifyContent='center'>
|
||||
<YStack
|
||||
flexGrow={1}
|
||||
justifyContent='center'
|
||||
paddingHorizontal={'$2'}
|
||||
maxHeight={'70%'}
|
||||
marginVertical={'auto'}
|
||||
>
|
||||
<Animated.View
|
||||
entering={FadeIn}
|
||||
exiting={FadeOut}
|
||||
key={`${nowPlaying!.item.AlbumId}-item-image`}
|
||||
>
|
||||
<ItemImage
|
||||
item={nowPlaying!.item}
|
||||
testID='player-image-test-id'
|
||||
width={360}
|
||||
height={360}
|
||||
/>
|
||||
<ItemImage item={nowPlaying!.item} testID='player-image-test-id' />
|
||||
</Animated.View>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { PlayerParamList } from '../../../screens/Player/types'
|
||||
import { useNowPlayingContext } from '../../../providers/Player'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { getItemName } from '../../../utils/text'
|
||||
|
||||
interface SongInfoProps {
|
||||
navigation: NativeStackNavigationProp<PlayerParamList>
|
||||
@@ -32,9 +33,12 @@ function SongInfo({ navigation }: SongInfoProps): React.JSX.Element {
|
||||
// Memoize expensive computations
|
||||
const trackTitle = useMemo(() => nowPlaying!.title ?? 'Untitled Track', [nowPlaying?.title])
|
||||
|
||||
const artistName = useMemo(() => nowPlaying?.artist ?? 'Unknown Artist', [nowPlaying?.artist])
|
||||
|
||||
const artistItems = useMemo(() => nowPlaying!.item.ArtistItems, [nowPlaying?.item.ArtistItems])
|
||||
const { artistItems, artists } = useMemo(() => {
|
||||
return {
|
||||
artistItems: nowPlaying!.item.ArtistItems,
|
||||
artists: nowPlaying!.item.ArtistItems?.map((artist) => getItemName(artist)).join(' • '),
|
||||
}
|
||||
}, [nowPlaying?.item.ArtistItems])
|
||||
|
||||
// Memoize navigation handlers
|
||||
const handleAlbumPress = useCallback(() => {
|
||||
@@ -88,7 +92,7 @@ function SongInfo({ navigation }: SongInfoProps): React.JSX.Element {
|
||||
|
||||
<TextTicker {...TextTickerConfig} style={{ height: getToken('$8') }}>
|
||||
<Text fontSize={'$6'} color={'$color'} onPress={handleArtistPress}>
|
||||
{artistName}
|
||||
{artists ?? 'Unknown Artist'}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
</YStack>
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import React, { useMemo, useCallback } from 'react'
|
||||
import {
|
||||
getToken,
|
||||
Progress,
|
||||
Spacer,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
XStack,
|
||||
YStack,
|
||||
ZStack,
|
||||
} from 'tamagui'
|
||||
import { getToken, Progress, View, XStack, YStack, ZStack } from 'tamagui'
|
||||
import { useNowPlayingContext } from '../../providers/Player'
|
||||
import { BottomTabNavigationEventMap } from '@react-navigation/bottom-tabs'
|
||||
import { NavigationHelpers, ParamListBase, useNavigation } from '@react-navigation/native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import PlayPauseButton from './components/buttons'
|
||||
import { ProgressMultiplier, TextTickerConfig } from './component.config'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { usePreviousContext, useSkipContext } from '../../providers/Player/queue'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { RunTimeSeconds } from '../Global/helpers/time-codes'
|
||||
@@ -31,9 +19,9 @@ import Animated, {
|
||||
useSharedValue,
|
||||
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'
|
||||
import ItemImage from '../Global/components/image'
|
||||
|
||||
export const Miniplayer = React.memo(function Miniplayer(): React.JSX.Element {
|
||||
const { api } = useJellifyContext()
|
||||
@@ -119,30 +107,10 @@ export const Miniplayer = React.memo(function Miniplayer(): React.JSX.Element {
|
||||
exiting={FadeOut}
|
||||
key={`${nowPlaying!.item.AlbumId}-album-image`}
|
||||
>
|
||||
<FastImage
|
||||
source={{
|
||||
uri:
|
||||
getImageApi(api)?.getItemImageUrlById(
|
||||
nowPlaying!.item.AlbumId! ||
|
||||
nowPlaying!.item.Id!,
|
||||
ImageType.Primary,
|
||||
{
|
||||
tag: nowPlaying!.item.ImageTags
|
||||
?.Primary,
|
||||
},
|
||||
) || '',
|
||||
}}
|
||||
style={{
|
||||
width: getToken('$12'),
|
||||
height: getToken('$12'),
|
||||
borderRadius: getToken('$2'),
|
||||
backgroundColor: '$borderColor',
|
||||
shadowRadius: getToken('$2'),
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: -getToken('$2'),
|
||||
},
|
||||
}}
|
||||
<ItemImage
|
||||
item={nowPlaying!.item}
|
||||
width={'$12'}
|
||||
height={'$12'}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
@@ -10,3 +10,9 @@ export const UPDATE_INTERVAL: number = 250
|
||||
* less than in order to do a skip to the previous
|
||||
*/
|
||||
export const SKIP_TO_PREVIOUS_THRESHOLD: number = 4
|
||||
|
||||
/**
|
||||
* Indicates the number of seconds the progress update
|
||||
* event will be emitted from the track player
|
||||
*/
|
||||
export const PROGRESS_UPDATE_EVENT_INTERVAL: number = 5
|
||||
|
||||
132
src/providers/Item/index.tsx
Normal file
132
src/providers/Item/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { createContext, ReactNode, useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../api/queries/media'
|
||||
import { useJellifyContext } from '..'
|
||||
import { useStreamingQualityContext } from '../Settings'
|
||||
import { getQualityParams } from '../../utils/mappings'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { ItemArtistProvider } from './item-artists'
|
||||
|
||||
interface ItemContext {
|
||||
item: BaseItemDto
|
||||
}
|
||||
|
||||
const ItemContext = createContext<ItemContext>({
|
||||
item: {},
|
||||
})
|
||||
|
||||
interface ItemProviderProps {
|
||||
item: BaseItemDto
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a series of {@link useQuery} functions that store additional context
|
||||
* around the item being browsed, including
|
||||
*
|
||||
* - Artist(s)
|
||||
* - Album
|
||||
* - Track(s)
|
||||
*
|
||||
* This data is used throughout Jellify as additional {@link useQuery} hooks to
|
||||
* tap into this cache
|
||||
*
|
||||
* @param param0 Object containing the {@link BaseItemDto} and the child {@link ReactNode} to render
|
||||
* @returns
|
||||
*/
|
||||
export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.Element = ({
|
||||
item,
|
||||
children,
|
||||
}) => {
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
const { Id, Type, AlbumId, ArtistItems } = item
|
||||
|
||||
const artistIds = ArtistItems?.map(({ Id }) => Id) ?? []
|
||||
|
||||
/**
|
||||
* Fetch and cache the media sources if this item is a track
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), Id!),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: !!Id && Type === BaseItemKind.Audio,
|
||||
})
|
||||
|
||||
/**
|
||||
* ...or store it as a queryable if the item is an artist
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ArtistById, Id],
|
||||
queryFn: () => item,
|
||||
enabled: !!Id && Type === BaseItemKind.MusicArtist,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire query for a track's album...
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.Album, AlbumId],
|
||||
queryFn: () => fetchItem(api, item.AlbumId!),
|
||||
enabled: !!AlbumId && Type === BaseItemKind.Audio,
|
||||
})
|
||||
|
||||
/**
|
||||
* ...or store it if it is an album
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.Album, Id],
|
||||
queryFn: () => item,
|
||||
enabled: !!Id && Type === BaseItemKind.MusicAlbum,
|
||||
})
|
||||
|
||||
/**
|
||||
* Prefetch for an album's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, Id],
|
||||
queryFn: () => fetchAlbumDiscs(api, item),
|
||||
enabled: !!Id && item.Type === BaseItemKind.MusicAlbum,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire query for a playlist's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, Id],
|
||||
queryFn: () =>
|
||||
getItemsApi(api!)
|
||||
.getItems({ parentId: Id! })
|
||||
.then(({ data }) => {
|
||||
if (data.Items) return data.Items
|
||||
else return []
|
||||
}),
|
||||
enabled: !!Id && Type === BaseItemKind.Playlist,
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<ItemContext.Provider value={{ item }}>
|
||||
{artistIds.map((Id) => Id && <ItemArtistProvider artistId={Id} key={Id} />)}
|
||||
{children}
|
||||
</ItemContext.Provider>
|
||||
),
|
||||
[item],
|
||||
)
|
||||
}
|
||||
32
src/providers/Item/item-artists.tsx
Normal file
32
src/providers/Item/item-artists.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createContext } from 'react'
|
||||
import { useJellifyContext } from '..'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchItem } from '../../api/queries/item'
|
||||
|
||||
interface ItemArtistContext {
|
||||
artistId: string | undefined
|
||||
}
|
||||
|
||||
const ItemArtistContext = createContext<ItemArtistContext>({
|
||||
artistId: undefined,
|
||||
})
|
||||
|
||||
export const ItemArtistProvider: ({
|
||||
artistId,
|
||||
}: {
|
||||
artistId: string | undefined
|
||||
}) => React.JSX.Element = ({ artistId }) => {
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
/**
|
||||
* Store queryable of artist item
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ArtistById, artistId],
|
||||
queryFn: () => fetchItem(api, artistId!),
|
||||
enabled: !!artistId,
|
||||
})
|
||||
|
||||
return <ItemArtistContext.Provider value={{ artistId }} />
|
||||
}
|
||||
@@ -151,13 +151,17 @@ const QueueContextInitailizer = () => {
|
||||
const shuffledInit = storage.getBoolean(MMKVStorageKeys.Shuffled)
|
||||
|
||||
//#region State
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(currentIndexValue ?? -1)
|
||||
const [playQueue, setPlayQueue] = useState<JellifyTrack[]>(playQueueInit)
|
||||
const [queueRef, setQueueRef] = useState<Queue>(queueRefInit)
|
||||
const [unshuffledQueue, setUnshuffledQueue] = useState<JellifyTrack[]>(unshuffledQueueInit)
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(currentIndexValue ?? -1)
|
||||
|
||||
/**
|
||||
* Handles whether we are loading in a new queue, in which case we will temporarily ignore
|
||||
* {@link Event.PlaybackActiveTrackChanged} events until that mutation has settled
|
||||
*/
|
||||
const [skipping, setSkipping] = useState<boolean>(false)
|
||||
const [initialized, setInitialized] = useState<boolean>(false)
|
||||
|
||||
const [shuffled, setShuffled] = useState<boolean>(shuffledInit ?? false)
|
||||
|
||||
@@ -177,6 +181,11 @@ const QueueContextInitailizer = () => {
|
||||
console.debug(`Active Track Changed to: ${index}. Skipping: ${skipping}`)
|
||||
if (skipping) return
|
||||
|
||||
// We get an event emitted when the queue is loaded from storage for the first time
|
||||
// This is the most convenient place I could find to flip this boolean and start
|
||||
// listening to emitted updates
|
||||
if (!initialized) return setInitialized(true)
|
||||
|
||||
let newIndex = -1
|
||||
|
||||
if (!isUndefined(track)) {
|
||||
@@ -262,10 +271,6 @@ const QueueContextInitailizer = () => {
|
||||
startIndex: number = 0,
|
||||
shuffleQueue: boolean = false,
|
||||
) => {
|
||||
trigger('impactLight')
|
||||
console.debug(`Queuing ${audioItems.length} items`)
|
||||
|
||||
setSkipping(true)
|
||||
setShuffled(shuffleQueue)
|
||||
|
||||
// Get the item at the start index
|
||||
@@ -535,7 +540,14 @@ const QueueContextInitailizer = () => {
|
||||
queue,
|
||||
shuffled,
|
||||
}: QueueMutation) => loadQueue(tracklist, queue, index, shuffled),
|
||||
onMutate: async ({ tracklist }) => {
|
||||
trigger('impactLight')
|
||||
console.debug(`Queuing ${tracklist.length} items`)
|
||||
|
||||
setSkipping(true)
|
||||
},
|
||||
onSuccess: async (data: void, { startPlayback }: QueueMutation) => {
|
||||
setInitialized(true)
|
||||
trigger('notificationSuccess')
|
||||
console.debug(`Loaded new queue`)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Progress, State } from 'react-native-track-player'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { PlaystateApi } from '@jellyfin/sdk/lib/generated-client/api/playstate-api'
|
||||
import { convertSecondsToRunTimeTicks } from '../../../utils/runtimeticks'
|
||||
import { PROGRESS_UPDATE_EVENT_INTERVAL } from '../../../player/config'
|
||||
|
||||
export async function handlePlaybackState(
|
||||
sessionId: string,
|
||||
@@ -49,7 +50,7 @@ export async function handlePlaybackProgress(
|
||||
) {
|
||||
if (playstateApi) {
|
||||
console.debug('Playback progress updated')
|
||||
if (Math.floor(progress.duration) - Math.floor(progress.position) <= 10) {
|
||||
if (shouldMarkPlaybackFinished(progress)) {
|
||||
console.debug(`Track finished. ${playstateApi ? 'scrobbling...' : ''}`)
|
||||
|
||||
if (playstateApi)
|
||||
@@ -75,3 +76,7 @@ export async function handlePlaybackProgress(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldMarkPlaybackFinished({ duration, position }: Progress): boolean {
|
||||
return Math.floor(duration) - Math.floor(position) <= PROGRESS_UPDATE_EVENT_INTERVAL
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
detachInactiveScreens={Platform.OS !== 'ios'} // Temp fix for iOS where screens are detaching
|
||||
initialRouteName={route.params?.screen ?? 'HomeTab'}
|
||||
screenOptions={{
|
||||
animation: 'shift',
|
||||
|
||||
@@ -8523,10 +8523,10 @@ react-native-safe-area-context@^5.6.0:
|
||||
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.0.tgz#0ab284c291bb57d59330abf7dfe65156d6340e78"
|
||||
integrity sha512-tJas3YOdsuCg3kepCTGF3LWZp9onMbb9Agju2xfs2kRX8d/5TMUPmupBpjerk/B7Tv/zeJnk+qp5neA96Y0otQ==
|
||||
|
||||
react-native-screens@4.14.1:
|
||||
version "4.14.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.14.1.tgz#7d440cb19752dc8dd46c07cca46222df10e4e38d"
|
||||
integrity sha512-/7zxVdk2H4BH/dvqpQQh45VCA05UeC+LCE8TPtGfjn5A+9/UJfKPB8LHhAcWxciLYfMCyW8J2u5dGLGQJH/Ecg==
|
||||
react-native-screens@4.15.0:
|
||||
version "4.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.15.0.tgz#8b34056b72a2c92da4de2f77419a116154c88e81"
|
||||
integrity sha512-LPz+9qWDfwY3pPKojrWPcQnb2sAq9cy/qA8ZAS14ksSdzqFkTTUbs1as2WGBo7xBtdx5Ht78bF9nNh8libX1Xw==
|
||||
dependencies:
|
||||
react-freeze "^1.0.0"
|
||||
react-native-is-edge-to-edge "^1.2.1"
|
||||
|
||||
Reference in New Issue
Block a user