Merge branch 'main' into Swipe-to-add-to-queue

This commit is contained in:
Violet Caulfield
2025-10-27 09:43:14 -05:00
committed by GitHub
46 changed files with 205 additions and 169 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
name: Bug report
about: Create a report to help us improve Jellify
title: "[BUG]"
labels: bug
type: bug
assignees: anultravioletaurora
---
@@ -2,6 +2,7 @@
name: Feature request
about: Suggest an idea for Jellify
title: "[FEATURE]"
type: feature
labels: enhancement
assignees: anultravioletaurora
+4 -2
View File
@@ -117,13 +117,15 @@ jobs:
- name: 🔑 Setup Play Store credentials
run: |
if [ -n "${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}" ]; then
echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}' > android/play-store-credentials.json
if [ -n "$PLAY_STORE_CREDENTIALS" ]; then
echo "$PLAY_STORE_CREDENTIALS" > android/play-store-credentials.json
echo "Play Store credentials created"
else
echo "ERROR: No Play Store credentials secret found!"
exit 1
fi
env:
PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
- name: 🤫 Output TelemetryDeck Secrets to TelemetryDeck.json
run: |
+3 -4
View File
@@ -1,6 +1,4 @@
# 🪼 Jellify
<img alt='Jellify logo' src='assets/icons/teal-icon.svg' width='250' height='250' /><br/>
<img alt='Jellify logo' src='assets/transparent-banner.png' width="600" height="300" /><br/>
[![Latest Version](https://img.shields.io/github/package-json/version/anultravioletaurora/jellify?label=Latest%20Version&color=indigo)](https://github.com/anultravioletaurora/Jellify/releases)
[![publish-beta](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml/badge.svg?branch=main)](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml) [![Publish Over-the-Air Update](https://github.com/Jellify-Music/App/actions/workflows/publish-ota-update.yml/badge.svg)](https://github.com/Jellify-Music/App/actions/workflows/publish-ota-update.yml)
@@ -211,6 +209,7 @@ Install via [Altstore](https://altstore.io) or your favorite sideloading utility
### 🎨 Frontend
[Tamagui](https://tamagui.dev/)\
[Figtree](https://github.com/erikdkennedy/figtree)\
[React Navigation](https://reactnavigation.org/)\
[React Native Blurhash](https://github.com/mrousavy/react-native-blurhash)\
[React Native CarPlay](https://github.com/birkir/react-native-carplay)\
@@ -281,7 +280,7 @@ This allows me to prioritize specific features, acquire additional hardware for
- Over-the-Air Updates
- Cast Support
- The friends we made along the way that have been critical in fostering an amazing community around _Jellify_
- [Thalia](https://github.com/PercyGabriel1129)
- [Thalia](https://github.com/thaliadavar)
- [BotBlake](https://github.com/BotBlake)
- [Neptune1987](https://github.com/NeptuneHub)
- My long time friends that have heard me talk about _Jellify_ for literally **eons**. Thank you for testing _Jellify_ during it's infancy and for supporting me all the way back at the beginning of this project
+2 -2
View File
@@ -91,8 +91,8 @@ android {
applicationId "com.jellify"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 131
versionName "0.18.4"
versionCode 132
versionName "0.18.5"
}
signingConfigs {
debug {
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+4 -4
View File
@@ -541,7 +541,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 243;
CURRENT_PROJECT_VERSION = 244;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
ENABLE_BITCODE = NO;
@@ -552,7 +552,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.18.4;
MARKETING_VERSION = 0.18.5;
NEW_SETTING = "";
OTHER_LDFLAGS = (
"$(inherited)",
@@ -583,7 +583,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 243;
CURRENT_PROJECT_VERSION = 244;
DEVELOPMENT_TEAM = WAH9CZ8BPG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -593,7 +593,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.18.4;
MARKETING_VERSION = 0.18.5;
NEW_SETTING = "";
OTHER_LDFLAGS = (
"$(inherited)",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "jellify",
"version": "0.18.4",
"version": "0.18.5",
"private": true,
"scripts": {
"init-android": "yarn install --network-concurrency 1",
@@ -44,7 +44,7 @@
"@react-navigation/bottom-tabs": "7.5.0",
"@react-navigation/material-top-tabs": "7.3.9",
"@react-navigation/native": "7.1.18",
"@react-navigation/native-stack": "7.5.0",
"@react-navigation/native-stack": "7.5.1",
"@sentry/react-native": "7.1.0",
"@shopify/flash-list": "^2.1.0",
"@tamagui/config": "1.135.4",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

-35
View File
@@ -1,35 +0,0 @@
import { Api } from '@jellyfin/sdk'
import { JellyfinInfo } from '../info'
import _ from 'lodash'
export function createApi(
serverUrl?: string,
username?: string,
password?: string,
accessToken?: string,
): Promise<Api> {
return new Promise((resolve, reject) => {
if (_.isUndefined(serverUrl)) {
console.info("Server Url doesn't exist yet")
return reject("Server Url doesn't exist")
}
if (!_.isUndefined(accessToken)) {
console.info('Creating API with accessToken')
return resolve(JellyfinInfo.createApi(serverUrl, accessToken))
}
if (_.isUndefined(username) && _.isUndefined(password)) {
console.info('Creating public API for server url')
return resolve(JellyfinInfo.createApi(serverUrl))
}
JellyfinInfo.createApi(serverUrl)
.authenticateUserByName(username!, password)
.then(({ data }) => {
if (data.AccessToken)
return resolve(JellyfinInfo.createApi(serverUrl, data.AccessToken))
else return reject('Unable to sign in')
})
})
}
+1 -1
View File
@@ -33,7 +33,7 @@ export const useFrequentlyPlayedArtists = () => {
return useInfiniteQuery({
queryKey: FrequentlyPlayedArtistsQueryKey(user, library),
queryFn: ({ pageParam }) => fetchFrequentlyPlayedArtists(api, library, pageParam),
queryFn: ({ pageParam }) => fetchFrequentlyPlayedArtists(api, user, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
+47 -34
View File
@@ -6,10 +6,14 @@ import {
} from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { Api } from '@jellyfin/sdk'
import { isUndefined } from 'lodash'
import { isEmpty, isNull, isUndefined } from 'lodash'
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
import { fetchItem } from '../../item'
import { ApiLimits } from '../../query.config'
import { JellifyUser } from '@/src/types/JellifyUser'
import { queryClient } from '../../../../constants/query-client'
import { InfiniteData } from '@tanstack/react-query'
import { FrequentlyPlayedTracksQueryKey } from '../keys'
/**
* Fetches the 100 most frequently played items from the user's library
@@ -58,6 +62,7 @@ export function fetchFrequentlyPlayed(
*/
export function fetchFrequentlyPlayedArtists(
api: Api | undefined,
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
page: number,
): Promise<BaseItemDto[]> {
@@ -69,41 +74,49 @@ export function fetchFrequentlyPlayedArtists(
if (isUndefined(api)) return reject('Client instance not set')
if (isUndefined(library)) return reject('Library instance not set')
fetchFrequentlyPlayed(api, library, 0)
.then((frequentTracks) => {
return frequentTracks
.filter((track) => !isUndefined(track.AlbumArtists))
.map((track) => {
return {
artistId: track.AlbumArtists![0].Id!,
playCount: track.UserData?.PlayCount ?? 0,
}
})
})
.then((albumArtistsWithPlayCounts) => {
return albumArtistsWithPlayCounts.reduce(
(acc, { artistId, playCount }) => {
const existing = acc.find((a) => a.artistId === artistId)
if (existing) {
existing.playCount += playCount
} else {
acc.push({ artistId, playCount })
}
return acc
},
[] as { artistId: string; playCount: number }[],
)
})
.then((artistsWithPlayCounts) => {
console.debug('Fetching artists')
const artists = artistsWithPlayCounts
.sort((a, b) => b.playCount - a.playCount)
.map((artist) => {
return fetchItem(api, artist.artistId)
})
const frequentlyPlayed = queryClient.getQueryData<InfiniteData<BaseItemDto[]>>(
FrequentlyPlayedTracksQueryKey(user, library),
)
if (isUndefined(frequentlyPlayed)) {
return reject('Frequently played tracks not found in query client')
}
return Promise.all(artists)
const artistIdWithPlayCount = frequentlyPlayed.pages[page]
.filter(
(track) =>
!isUndefined(track.AlbumArtists) &&
!isNull(track.AlbumArtists) &&
!isEmpty(track.AlbumArtists) &&
!isUndefined(track.AlbumArtists![0].Id),
)
.map(({ AlbumArtists, UserData }) => {
return {
artistId: AlbumArtists![0].Id!,
playCount: UserData?.PlayCount ?? 0,
}
})
console.info('Artist IDs with play count:', artistIdWithPlayCount.length)
const artistPromises = artistIdWithPlayCount
.reduce(
(acc, { artistId, playCount }) => {
const existing = acc.find((a) => a.artistId === artistId)
if (existing) {
existing.playCount += playCount
} else {
acc.push({ artistId, playCount })
}
return acc
},
[] as { artistId: string; playCount: number }[],
)
.sort((a, b) => b.playCount - a.playCount)
.map((artist) => {
return fetchItem(api, artist.artistId)
})
return Promise.all(artistPromises)
.then((artists) => {
return resolve(artists.filter((artist) => !isUndefined(artist)))
})
+2 -2
View File
@@ -27,7 +27,7 @@ export async function fetchItem(api: Api | undefined, itemId: string): Promise<B
getItemsApi(api)
.getItems({
ids: [itemId],
fields: [ItemFields.Tags],
fields: [ItemFields.Tags, ItemFields.Genres],
enableUserData: true,
})
.then((response) => {
@@ -77,7 +77,7 @@ export async function fetchItems(
sortBy,
recursive: true,
sortOrder,
fields: [ItemFields.ChildCount, ItemFields.SortName],
fields: [ItemFields.ChildCount, ItemFields.SortName, ItemFields.Genres],
startIndex: typeof page === 'number' ? page * QueryConfig.limits.library : 0,
limit: QueryConfig.limits.library,
isFavorite,
+6 -1
View File
@@ -1,11 +1,16 @@
import { ONE_DAY } from '../../constants/query-client'
import { ImageFormat } from '@jellyfin/sdk/lib/generated-client/models'
export enum ApiLimits {
Discover = 50,
Home = 100,
Library = 400,
Discover = 50,
Similar = 5,
}
/**
* @deprecated Create an enumeration for the configuration needed.
*/
const QueryConfig = {
/**
* Defines the limits for the number of items returned by a query
+1
View File
@@ -6,6 +6,7 @@ import { ApiLimits } from '../query.config'
import { isUndefined } from 'lodash'
const RECENTS_QUERY_CONFIG = {
maxPages: 2,
refetchOnMount: false,
staleTime: Infinity,
} as const
+2 -2
View File
@@ -1,6 +1,6 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api'
import QueryConfig from './query.config'
import { ApiLimits } from './query.config'
import { Api } from '@jellyfin/sdk'
import { isUndefined } from 'lodash'
import { JellifyUser } from '../../types/JellifyUser'
@@ -10,7 +10,7 @@ export default function fetchSimilar(
user: JellifyUser | undefined,
libraryId: string | undefined,
itemId: string,
limit: number = QueryConfig.limits.similar,
limit: number = ApiLimits.Similar,
startIndex: number = 0,
): Promise<BaseItemDto[]> {
return new Promise((resolve, reject) => {
+2 -1
View File
@@ -32,6 +32,7 @@ export async function fetchSearchSuggestions(
BaseItemKind.MusicAlbum,
],
sortBy: ['IsFavoriteOrLiked', 'Random'],
fields: [ItemFields.ChildCount, ItemFields.SortName, ItemFields.Genres],
})
.then(({ data }) => {
if (data.Items) resolve(data.Items)
@@ -60,7 +61,7 @@ export async function fetchArtistSuggestions(
userId: user.id,
limit: 50,
startIndex: page * 50,
fields: [ItemFields.ChildCount, ItemFields.SortName],
fields: [ItemFields.ChildCount, ItemFields.SortName, ItemFields.Genres],
sortBy: ['Random'],
})
.then(({ data }) => {
+8 -10
View File
@@ -56,15 +56,13 @@ export default function ArtistNavigation({
)
return (
<SafeAreaView edges={['right', 'left']}>
<SectionList
contentInsetAdjustmentBehavior='automatic'
sections={sections}
ListHeaderComponent={ArtistHeader}
renderSectionHeader={renderSectionHeader}
renderItem={({ item }) => <ItemRow item={item} navigation={navigation} />}
ListFooterComponent={SimilarArtists}
/>
</SafeAreaView>
<SectionList
contentInsetAdjustmentBehavior='automatic'
sections={sections}
ListHeaderComponent={ArtistHeader}
renderSectionHeader={renderSectionHeader}
renderItem={({ item }) => <ItemRow item={item} navigation={navigation} />}
ListFooterComponent={SimilarArtists}
/>
)
}
+5 -15
View File
@@ -1,13 +1,12 @@
import { ItemCard } from '../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../screens/types'
import { useNavigation } from '@react-navigation/native'
import { Text } from '../Global/helpers/text'
import { useArtistContext } from '../../providers/Artist'
import { ActivityIndicator } from 'react-native'
import navigationRef from '../../../navigation'
import HorizontalCardList from '../Global/components/horizontal-list'
import { H6, YStack } from 'tamagui'
import { YStack } from 'tamagui'
import { FlashList } from '@shopify/flash-list'
import ItemRow from '../Global/components/item-row'
export default function SimilarArtists(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
@@ -21,24 +20,16 @@ export default function SimilarArtists(): React.JSX.Element {
bold
>{`Similar to ${artist.Name ?? 'Unknown Artist'}`}</Text>
<HorizontalCardList
<FlashList
data={similarArtists}
renderItem={({ item: artist }) => (
<ItemCard
caption={artist.Name ?? 'Unknown Artist'}
size={'$8'}
<ItemRow
item={artist}
onPress={() => {
navigation.push('Artist', {
artist,
})
}}
onLongPress={() => {
navigationRef.navigate('Context', {
item: artist,
navigation,
})
}}
/>
)}
ListEmptyComponent={
@@ -50,7 +41,6 @@ export default function SimilarArtists(): React.JSX.Element {
</Text>
)
}
removeClippedSubviews
/>
</YStack>
)
+1
View File
@@ -15,6 +15,7 @@ export default function Index(): React.JSX.Element {
contentContainerStyle={{
flexGrow: 1,
marginTop: getToken('$4'),
marginHorizontal: getToken('$2'),
}}
contentInsetAdjustmentBehavior='automatic'
removeClippedSubviews
@@ -2,7 +2,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { ItemCard } from '../../../components/Global/components/item-card'
import { useDiscoverContext } from '../../../providers/Discover'
import { View, XStack } from 'tamagui'
import { H5, View, XStack } from 'tamagui'
import { H4 } from '../../../components/Global/helpers/text'
import Icon from '../../Global/components/icon'
import { useNavigation } from '@react-navigation/native'
@@ -25,7 +25,7 @@ export default function RecentlyAdded(): React.JSX.Element {
})
}}
>
<H4 marginLeft={'$2'}>Recently Added</H4>
<H5 marginLeft={'$2'}>Recently Added</H5>
<Icon name='arrow-right' />
</XStack>
@@ -36,7 +36,7 @@ export default function RecentlyAdded(): React.JSX.Element {
caption={item.Name}
subCaption={`${item.Artists?.join(', ')}`}
squared
size={'$10'}
size={'$11'}
item={item}
onPress={() => {
navigation.navigate('Album', {
@@ -49,6 +49,8 @@ export default function RecentlyAdded(): React.JSX.Element {
navigation,
})
}}
gap={'$1'}
captionAlign='left'
/>
)}
/>
@@ -1,4 +1,4 @@
import { View, XStack } from 'tamagui'
import { H5, View, XStack } from 'tamagui'
import { useDiscoverContext } from '../../../providers/Discover'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Icon from '../../Global/components/icon'
@@ -41,9 +41,9 @@ export default function PublicPlaylists() {
})
}}
>
<H4 marginLeft={'$2'} lineBreakStrategyIOS='standard' maxWidth={width * 0.8}>
<H5 marginLeft={'$2'} lineBreakStrategyIOS='standard' maxWidth={width * 0.8}>
Playlists on {server?.name ?? 'Jellyfin'}
</H4>
</H5>
<Icon name='arrow-right' />
</XStack>
<HorizontalCardList
@@ -64,6 +64,8 @@ export default function PublicPlaylists() {
navigation,
})
}
marginHorizontal={'$1'}
captionAlign='left'
/>
)}
/>
@@ -1,13 +1,13 @@
import { View, XStack } from 'tamagui'
import { H5, View, XStack } from 'tamagui'
import Icon from '../../Global/components/icon'
import HorizontalCardList from '../../Global/components/horizontal-list'
import { ItemCard } from '../../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useDiscoverContext } from '../../../providers/Discover'
import { H4 } from '../../Global/helpers/text'
import { useNavigation } from '@react-navigation/native'
import DiscoverStackParamList from '../../../screens/Discover/types'
import navigationRef from '../../../../navigation'
import { pickFirstGenre } from '../../../utils/genre-formatting'
export default function SuggestedArtists(): React.JSX.Element {
const { suggestedArtistsInfiniteQuery } = useDiscoverContext()
@@ -26,7 +26,7 @@ export default function SuggestedArtists(): React.JSX.Element {
}}
marginLeft={'$2'}
>
<H4>Suggested Artists</H4>
<H5>Suggested Artists</H5>
<Icon name='arrow-right' />
</XStack>
<HorizontalCardList
@@ -34,6 +34,7 @@ export default function SuggestedArtists(): React.JSX.Element {
renderItem={({ item }) => (
<ItemCard
caption={item.Name}
subCaption={pickFirstGenre(item.Genres)}
size={'$10'}
item={item}
onPress={() => {
+3 -3
View File
@@ -114,7 +114,7 @@ function Image({
? getBorderRadius(circular, width)
: circular
? getTokenValue('$20') * 10
: getTokenValue('$2'),
: getTokenValue('$5'),
width: !isUndefined(width)
? typeof width === 'number'
? width
@@ -180,8 +180,8 @@ function getBorderRadius(
? width / 25
: typeof width === 'string' && width.includes('%')
? 0
: getTokenValue(width as Token) / 15
} else borderRadius = getTokenValue('$2')
: getTokenValue(width as Token) / 10
} else borderRadius = getTokenValue('$10')
return borderRadius
}
+16 -3
View File
@@ -13,6 +13,7 @@ interface CardProps extends TamaguiCardProps {
item: BaseItemDto
squared?: boolean
testId?: string | null | undefined
captionAlign?: 'center' | 'left' | 'right'
}
/**
@@ -29,6 +30,7 @@ function ItemCardComponent({
squared,
testId,
onPress,
captionAlign = 'center',
...cardProps
}: CardProps) {
usePerformanceMonitor('ItemCard', 2)
@@ -74,13 +76,24 @@ function ItemCardComponent({
</TamaguiCard.Background>
</TamaguiCard>
{caption && (
<YStack alignContent='center' alignItems='center' maxWidth={cardProps.size}>
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
<YStack maxWidth={cardProps.size}>
<Text
bold
lineBreakStrategyIOS='standard'
width={cardProps.size}
numberOfLines={1}
textAlign={captionAlign}
>
{caption}
</Text>
{subCaption && (
<Text lineBreakStrategyIOS='standard' numberOfLines={1} textAlign='center'>
<Text
lineBreakStrategyIOS='standard'
width={cardProps.size}
numberOfLines={1}
textAlign={captionAlign}
>
{subCaption}
</Text>
)}
@@ -225,7 +225,7 @@ function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element {
</XStack>
)}
{shouldRenderGenres && (
{shouldRenderGenres && item.Genres && (
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
{item.Genres?.join(', ') ?? ''}
</Text>
@@ -2,8 +2,7 @@ import HorizontalCardList from '../../../components/Global/components/horizontal
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React, { useCallback } from 'react'
import { ItemCard } from '../../../components/Global/components/item-card'
import { View, XStack } from 'tamagui'
import { H4 } from '../../../components/Global/helpers/text'
import { H5, View, XStack } from 'tamagui'
import Icon from '../../Global/components/icon'
import { useDisplayContext } from '../../../providers/Display/display-provider'
import { useNavigation } from '@react-navigation/native'
@@ -11,6 +10,7 @@ import HomeStackParamList from '../../../screens/Home/types'
import { RootStackParamList } from '../../../screens/types'
import { useFrequentlyPlayedArtists } from '../../../api/queries/frequents'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import { pickFirstGenre } from '../../../utils/genre-formatting'
export default function FrequentArtists(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
@@ -24,6 +24,7 @@ export default function FrequentArtists(): React.JSX.Element {
<ItemCard
item={artist}
caption={artist.Name ?? 'Unknown Artist'}
subCaption={pickFirstGenre(artist.Genres)}
onPress={() => {
navigation.navigate('Artist', {
artist,
@@ -51,7 +52,7 @@ export default function FrequentArtists(): React.JSX.Element {
})
}}
>
<H4 marginLeft={'$2'}>Most Played</H4>
<H5 marginLeft={'$2'}>Most Played</H5>
<Icon name='arrow-right' />
</XStack>
@@ -1,5 +1,5 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { View, XStack } from 'tamagui'
import { H5, View, XStack } from 'tamagui'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { ItemCard } from '../../../components/Global/components/item-card'
import { QueuingType } from '../../../enums/queuing-type'
@@ -41,7 +41,7 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
})
}}
>
<H4 marginLeft={'$2'}>On Repeat</H4>
<H5 marginLeft={'$2'}>On Repeat</H5>
<Icon name='arrow-right' />
</XStack>
@@ -54,7 +54,7 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
renderItem={({ item: track, index }) => (
<ItemCard
item={track}
size={'$10'}
size={'$11'}
caption={track.Name}
subCaption={`${track.Artists?.join(', ')}`}
squared
@@ -77,6 +77,8 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
navigation,
})
}}
marginHorizontal={'$1'}
captionAlign='left'
/>
)}
/>
@@ -1,6 +1,5 @@
import React from 'react'
import { View, XStack } from 'tamagui'
import { H4 } from '../../Global/helpers/text'
import { H5, View, XStack } from 'tamagui'
import { RootStackParamList } from '../../../screens/types'
import { ItemCard } from '../../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
@@ -10,6 +9,7 @@ import { useDisplayContext } from '../../../providers/Display/display-provider'
import { useNavigation } from '@react-navigation/native'
import HomeStackParamList from '../../../screens/Home/types'
import { useRecentArtists } from '../../../api/queries/recents'
import { pickFirstGenre } from '../../../utils/genre-formatting'
export default function RecentArtists(): React.JSX.Element {
const recentArtistsInfiniteQuery = useRecentArtists()
@@ -29,7 +29,7 @@ export default function RecentArtists(): React.JSX.Element {
})
}}
>
<H4 marginLeft={'$2'}>Recent Artists</H4>
<H5 marginLeft={'$2'}>Recent Artists</H5>
<Icon name='arrow-right' />
</XStack>
@@ -39,6 +39,7 @@ export default function RecentArtists(): React.JSX.Element {
<ItemCard
item={recentArtist}
caption={recentArtist.Name ?? 'Unknown Artist'}
subCaption={pickFirstGenre(recentArtist.Genres)}
onPress={() => {
navigation.navigate('Artist', {
artist: recentArtist,
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react'
import { View, XStack } from 'tamagui'
import { H5, View, XStack } from 'tamagui'
import { H4 } from '../../Global/helpers/text'
import { ItemCard } from '../../Global/components/item-card'
import { RootStackParamList } from '../../../screens/types'
@@ -45,7 +45,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
})
}}
>
<H4 marginLeft={'$2'}>Play it again</H4>
<H5 marginLeft={'$2'}>Play it again</H5>
<Icon name='arrow-right' />
</XStack>
@@ -57,7 +57,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
}
renderItem={({ index, item: recentlyPlayedTrack }) => (
<ItemCard
size={'$10'}
size={'$11'}
caption={recentlyPlayedTrack.Name}
subCaption={`${recentlyPlayedTrack.Artists?.join(', ')}`}
squared
@@ -82,6 +82,8 @@ export default function RecentlyPlayed(): React.JSX.Element {
navigation,
})
}}
marginHorizontal={'$1'}
captionAlign='left'
/>
)}
/>
+7 -2
View File
@@ -1,4 +1,4 @@
import { ScrollView, RefreshControl } from 'react-native'
import { ScrollView, RefreshControl, Platform } from 'react-native'
import { YStack, getToken } from 'tamagui'
import RecentArtists from './helpers/recent-artists'
import RecentlyPlayed from './helpers/recently-played'
@@ -22,10 +22,15 @@ export function Home(): React.JSX.Element {
contentInsetAdjustmentBehavior='automatic'
contentContainerStyle={{
marginVertical: getToken('$4'),
marginHorizontal: getToken('$2'),
}}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}
>
<YStack alignContent='flex-start' gap='$3'>
<YStack
alignContent='flex-start'
gap='$3'
marginBottom={Platform.OS === 'android' ? '$4' : undefined}
>
<RecentArtists />
<RecentlyPlayed />
+5 -8
View File
@@ -22,19 +22,18 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
{[''].includes(props.state.routes[props.state.index].name) ? null : (
<XStack
paddingHorizontal={'$4'}
borderWidth={'$1'}
borderColor={'$borderColor'}
marginTop={'$2'}
marginHorizontal={'$2'}
borderRadius={'$4'}
backgroundColor={'$background'}
alignItems={'center'}
justifyContent='flex-end'
justifyContent='flex-start'
paddingHorizontal={'$4'}
paddingVertical={'$1'}
gap={'$4'}
maxWidth={'80%'}
>
{props.state.routes[props.state.index].name === 'Playlists' ? (
<XStack
flex={1}
onPress={() => {
trigger('impactLight')
props.navigation.navigate('AddPlaylist')
@@ -48,7 +47,6 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
</XStack>
) : (
<XStack
flex={1}
onPress={() => {
trigger('impactLight')
setIsFavorites(!isUndefined(isFavorites) ? undefined : true)
@@ -69,7 +67,6 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
{props.state.routes[props.state.index].name === 'Tracks' && (
<XStack
flex={1}
onPress={() => {
trigger('impactLight')
setIsDownloaded(!isDownloaded)
+3 -1
View File
@@ -16,6 +16,8 @@ export default function PlayerHeader(): React.JSX.Element {
const theme = useTheme()
const artworkMaxHeight = Platform.OS === 'android' ? '65%' : '70%'
// If the Queue is a BaseItemDto, display the name of it
const playingFrom = useMemo(
() =>
@@ -56,7 +58,7 @@ export default function PlayerHeader(): React.JSX.Element {
flexGrow={1}
justifyContent='center'
paddingHorizontal={'$2'}
maxHeight={'70%'}
maxHeight={artworkMaxHeight}
marginVertical={'auto'}
>
<Animated.View
+12
View File
@@ -0,0 +1,12 @@
import axios from 'axios'
/**
* The Axios instance for making HTTP requests.
*
* Default timeout is set to 15 seconds.
*/
const AXIOS_INSTANCE = axios.create({
timeout: 15 * 1000, // 15 seconds
})
export default AXIOS_INSTANCE
+2 -2
View File
@@ -29,9 +29,9 @@ export const queryClient = new QueryClient({
gcTime: ONE_DAY,
/**
* Refetch data after 2 hours as a default
* Refetch data after 12 hours as a default
*/
staleTime: ONE_HOUR * 2,
staleTime: ONE_HOUR * 12,
refetchIntervalInBackground: false,
+2 -2
View File
@@ -47,8 +47,6 @@ export const PlayerProvider: () => React.JSX.Element = () => {
switch (event.type) {
case Event.PlaybackActiveTrackChanged:
await handleActiveTrackChanged()
if (event.track) {
nowPlaying = event.track as JellifyTrack
@@ -56,6 +54,8 @@ export const PlayerProvider: () => React.JSX.Element = () => {
await TrackPlayer.setVolume(volume)
}
await handleActiveTrackChanged()
if (event.lastTrack)
if (isPlaybackFinished(event.lastPosition, event.lastTrack.duration ?? 1))
await reportPlaybackCompleted(api, event.lastTrack as JellifyTrack)
+4 -2
View File
@@ -16,6 +16,7 @@ import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'
import { Api } from '@jellyfin/sdk/lib/api'
import { JellyfinInfo } from '../api/info'
import { queryClient } from '../constants/query-client'
import AXIOS_INSTANCE from '../configs/axios.config'
/**
* The context for the Jellify provider.
@@ -99,8 +100,9 @@ const JellifyContextInitializer = () => {
useEffect(() => {
if (!isUndefined(server) && !isUndefined(user))
setApi(JellyfinInfo.createApi(server.url, user.accessToken))
else if (!isUndefined(server)) setApi(JellyfinInfo.createApi(server.url))
setApi(JellyfinInfo.createApi(server.url, user.accessToken, AXIOS_INSTANCE))
else if (!isUndefined(server))
setApi(JellyfinInfo.createApi(server.url, undefined, AXIOS_INSTANCE))
else setApi(undefined)
setLoggedIn(!isUndefined(server) && !isUndefined(user) && !isUndefined(library))
+5
View File
@@ -10,6 +10,7 @@ import LibraryScreen from '../Library'
import TabParamList from './types'
import { TabProps } from '../types'
import TabBar from './tab-bar'
import { Platform } from 'react-native'
const Tab = createBottomTabNavigator<TabParamList>()
@@ -18,6 +19,10 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element
return (
<Tab.Navigator
/*
* https://github.com/react-navigation/react-navigation/issues/12755
*/
detachInactiveScreens={Platform.OS !== 'ios'}
initialRouteName={route.params?.screen ?? 'HomeTab'}
screenOptions={{
animation: 'shift',
+13
View File
@@ -0,0 +1,13 @@
/**
*
* @param genres A list of Genres returned by the Jellyfin API
* @returns A the first, singular genre the array
* @example Kim Petras - "Dance"
*/
export function pickFirstGenre(genres: string[] | undefined | null): string {
if (!genres || genres.length === 0) {
return ''
}
return genres[0].split(';')[0]
}
+4 -4
View File
@@ -23,10 +23,10 @@ const tokens = createTokens({
white: '#ffffff',
neutral: '#77748E',
darkBackground: 'rgb(17, 16, 20)',
darkBackground75: 'rgba(17, 16, 20, 0.75)',
darkBackground50: 'rgba(17, 16, 20, 0.5)',
darkBackground25: 'rgba(17, 16, 20, 0.25)',
darkBackground: 'rgba(25, 24, 28, 1)',
darkBackground75: 'rgba(25, 24, 28, 0.75)',
darkBackground50: 'rgba(25, 24, 28, 0.5)',
darkBackground25: 'rgba(25, 24, 28, 0.25)',
darkBorder: '#CEAAFF',
+4 -4
View File
@@ -2167,10 +2167,10 @@
color "^4.2.3"
react-native-tab-view "^4.2.0"
"@react-navigation/native-stack@7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.5.0.tgz#413e586cf5d0615aad35fe48493975b0073dfc81"
integrity sha512-boUYXqEsI+plBTfnCOorW7v+lLk/B+0875hZ5qdK6C4cItgGzle2y+tU1dlfQCRszyXlz4l/qwJwTLFwUqUGDg==
"@react-navigation/native-stack@7.5.1":
version "7.5.1"
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.5.1.tgz#8b184a65c49bcd21d859ac06ae9c033e0df319b7"
integrity sha512-OTn+thYqa5z43j8CyVsi/pTAAZHj27ly/fsd9zz8l8ypxoGhYz7Ro0+Gb9MsL+9Yw1QJTXzJ8dk41+Ay1MafKg==
dependencies:
"@react-navigation/elements" "^2.7.0"
color "^4.2.3"