style: center align header titles for Discover, Home, and Search screens (#784)

* style: increase margin for various components to improve layout consistency

* style: center align header titles for Discover, Home, and Search screens

* style: increase header title font size to improve readability across Discover, Home, and Search screens

* style: reduce left margin for header titles in Frequent Artists, Frequently Played Tracks, Recent Artists, and Recently Played components

* style: filter music libraries and auto-select if only one is available; add no libraries found message

* style: simplify favorites toggle logic in LibraryTabBar component

---------

Co-authored-by: Violet Caulfield <violet@cosmonautical.cloud>
This commit is contained in:
skalthoff
2025-12-11 05:10:03 -08:00
committed by GitHub
parent 47632386f1
commit 96e2bec558
6 changed files with 154 additions and 101 deletions

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Spinner, ToggleGroup, XStack, YStack } from 'tamagui' import { H3, Spinner, ToggleGroup, XStack, YStack } from 'tamagui'
import { H2, Text } from '../helpers/text' import { H2, Text } from '../helpers/text'
import Button from '../helpers/button' import Button from '../helpers/button'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
@@ -9,6 +9,13 @@ import { fetchUserViews } from '../../../api/queries/libraries'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import Icon from './icon' import Icon from './icon'
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores' import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
import Animated, {
FadeIn,
FadeInUp,
FadeOut,
FadeOutUp,
LinearTransition,
} from 'react-native-reanimated'
interface LibrarySelectorProps { interface LibrarySelectorProps {
onLibrarySelected: ( onLibrarySelected: (
@@ -73,9 +80,15 @@ export default function LibrarySelector({
useEffect(() => { useEffect(() => {
if (!isPending && isSuccess && libraries) { if (!isPending && isSuccess && libraries) {
setMusicLibraries( const filteredMusicLibraries = libraries.filter(
libraries.filter((library) => library.CollectionType === CollectionType.Music), (library) => library.CollectionType === CollectionType.Music,
) )
setMusicLibraries(filteredMusicLibraries)
// Auto-select if there's only one music library
if (filteredMusicLibraries.length === 1 && !selectedLibraryId) {
setSelectedLibraryId(filteredMusicLibraries[0].Id)
}
// Find the playlist library // Find the playlist library
const foundPlaylistLibrary = libraries.find( const foundPlaylistLibrary = libraries.find(
@@ -86,9 +99,7 @@ export default function LibrarySelector({
} }
}, [isPending, isSuccess, libraries]) }, [isPending, isSuccess, libraries])
const libraryToggleItems = useMemo( const libraryToggleItems = musicLibraries.map((library) => {
() =>
musicLibraries.map((library) => {
const isSelected: boolean = selectedLibraryId === library.Id! const isSelected: boolean = selectedLibraryId === library.Id!
return ( return (
@@ -100,6 +111,8 @@ export default function LibrarySelector({
scale: 0.9, scale: 0.9,
}} }}
backgroundColor={isSelected ? '$primary' : '$background'} backgroundColor={isSelected ? '$primary' : '$background'}
borderWidth={hasMultipleLibraries ? 1 : 0}
borderColor={isSelected ? '$primary' : '$borderColor'}
> >
<Text <Text
fontWeight={isSelected ? 'bold' : '600'} fontWeight={isSelected ? 'bold' : '600'}
@@ -109,38 +122,52 @@ export default function LibrarySelector({
</Text> </Text>
</ToggleGroup.Item> </ToggleGroup.Item>
) )
}), })
[selectedLibraryId, musicLibraries],
)
return ( return (
<SafeAreaView style={{ flex: 1 }}>
<YStack <YStack
flex={1} flex={1}
justifyContent='center' justifyContent='center'
paddingHorizontal={'$4'} paddingHorizontal={'$4'}
marginBottom={isOnboarding ? '$20' : 'unset'} marginBottom={isOnboarding ? '$20' : '$4'}
> >
<YStack flex={1} alignItems='center' justifyContent='flex-end'> <Animated.View
<H2 textAlign='center' marginBottom={'$2'}> entering={FadeInUp.springify()}
exiting={FadeOutUp.springify()}
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'flex-end',
}}
>
<H3 textAlign='center' marginBottom={'$2'}>
{title} {title}
</H2> </H3>
</Animated.View>
{!hasMultipleLibraries && !isOnboarding && ( {!hasMultipleLibraries && !isOnboarding && (
<Animated.View entering={FadeIn.springify()} exiting={FadeOut.springify()}>
<Text color='$borderColor' textAlign='center'> <Text color='$borderColor' textAlign='center'>
Only one music library is available Only one music library is available
</Text> </Text>
</Animated.View>
)} )}
</YStack>
<YStack justifyContent='center' flexGrow={1} minHeight={'$12'} gap={'$4'}> <Animated.View
style={{
justifyContent: 'center',
flexGrow: 1,
}}
>
{isPending ? ( {isPending ? (
<Spinner size='large' /> <Spinner size='large' enterStyle={{ opacity: 1 }} exitStyle={{ opacity: 0 }} />
) : isError ? ( ) : isError ? (
<Text color='$danger' textAlign='center'> <LoadErrorMessage />
Unable to load libraries ) : musicLibraries.length === 0 ? (
</Text> <NoLibrariesMessage />
) : ( ) : (
<ToggleGroup <ToggleGroup
enterStyle={{ opacity: 1 }}
exitStyle={{ opacity: 0 }}
orientation='vertical' orientation='vertical'
type='single' type='single'
animation={'quick'} animation={'quick'}
@@ -152,7 +179,7 @@ export default function LibrarySelector({
{libraryToggleItems} {libraryToggleItems}
</ToggleGroup> </ToggleGroup>
)} )}
</YStack> </Animated.View>
<XStack alignItems='flex-end' gap={'$3'} marginTop={'$4'}> <XStack alignItems='flex-end' gap={'$3'} marginTop={'$4'}>
{showCancelButton && ( {showCancelButton && (
@@ -180,6 +207,27 @@ export default function LibrarySelector({
</Button> </Button>
</XStack> </XStack>
</YStack> </YStack>
</SafeAreaView> )
}
function LoadErrorMessage(): React.JSX.Element {
return (
<Text color='$danger' textAlign='center'>
Unable to load libraries
</Text>
)
}
function NoLibrariesMessage(): React.JSX.Element {
return (
<YStack alignItems='center' gap={'$2'}>
<Icon name='alert' color='$danger' />
<Text color='$danger' textAlign='center'>
No music libraries found
</Text>
<Text color='$borderColor' textAlign='center' fontSize={'$3'}>
Please create a music library in Jellyfin to continue
</Text>
</YStack>
) )
} }

View File

@@ -75,7 +75,6 @@ export default function Search({
placeholder='Seek and ye shall find' placeholder='Seek and ye shall find'
onChangeText={(value) => handleSearchStringUpdate(value)} onChangeText={(value) => handleSearchStringUpdate(value)}
value={searchString} value={searchString}
marginHorizontal={'$2'}
testID='search-input' testID='search-input'
clearButtonMode='while-editing' clearButtonMode='while-editing'
/> />
@@ -149,7 +148,7 @@ export default function Search({
renderItem={({ item }) => <ItemRow item={item} navigation={navigation} />} renderItem={({ item }) => <ItemRow item={item} navigation={navigation} />}
onScrollBeginDrag={handleScrollBeginDrag} onScrollBeginDrag={handleScrollBeginDrag}
style={{ style={{
marginHorizontal: getToken('$2'), marginHorizontal: getToken('$4'),
marginTop: getToken('$4'), marginTop: getToken('$4'),
}} }}
/> />

View File

@@ -1,6 +1,6 @@
import ItemRow from '../Global/components/item-row' import ItemRow from '../Global/components/item-row'
import { Text } from '../Global/helpers/text' import { Text } from '../Global/helpers/text'
import { H3, Separator, YStack } from 'tamagui' import { H5, Separator, Spinner, YStack } from 'tamagui'
import { ItemCard } from '../Global/components/item-card' import { ItemCard } from '../Global/components/item-card'
import HorizontalCardList from '../Global/components/horizontal-list' import HorizontalCardList from '../Global/components/horizontal-list'
import { FlashList } from '@shopify/flash-list' import { FlashList } from '@shopify/flash-list'
@@ -26,7 +26,7 @@ export default function Suggestions({
data={suggestions?.filter((suggestion) => suggestion.Type !== 'MusicArtist')} data={suggestions?.filter((suggestion) => suggestion.Type !== 'MusicArtist')}
ListHeaderComponent={ ListHeaderComponent={
<YStack> <YStack>
<H3>Suggestions</H3> <H5>Suggestions</H5>
<HorizontalCardList <HorizontalCardList
data={suggestions?.filter( data={suggestions?.filter(
@@ -51,17 +51,17 @@ export default function Suggestions({
} }
ItemSeparatorComponent={() => <Separator />} ItemSeparatorComponent={() => <Separator />}
ListEmptyComponent={ ListEmptyComponent={
<YStack justifyContent='center' alignContent='center'>
<Text textAlign='center'> <Text textAlign='center'>
Wake now, discover that you are the eyes of the world... Wake now, discover that you are the eyes of the world...
</Text> </Text>
<Spinner color={'$primary'} />
</YStack>
} }
onScrollBeginDrag={handleScrollBeginDrag} onScrollBeginDrag={handleScrollBeginDrag}
renderItem={({ item }) => { renderItem={({ item }) => {
return <ItemRow item={item} navigation={navigation} /> return <ItemRow item={item} navigation={navigation} />
}} }}
style={{
marginHorizontal: 2,
}}
/> />
) )
} }

View File

@@ -2,7 +2,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
import Index from '../../components/Discover/component' import Index from '../../components/Discover/component'
import AlbumScreen from '../Album' import AlbumScreen from '../Album'
import { ArtistScreen } from '../Artist' import { ArtistScreen } from '../Artist'
import { useTheme } from 'tamagui' import { getTokenValue, useTheme } from 'tamagui'
import RecentlyAdded from './albums' import RecentlyAdded from './albums'
import PublicPlaylists from './playlists' import PublicPlaylists from './playlists'
import { PlaylistScreen } from '../Playlist' import { PlaylistScreen } from '../Playlist'
@@ -22,8 +22,10 @@ export function Discover(): React.JSX.Element {
name='Discover' name='Discover'
component={Index} component={Index}
options={{ options={{
headerTitleAlign: 'center',
headerTitleStyle: { headerTitleStyle: {
fontFamily: 'Figtree-Bold', fontFamily: 'Figtree-Bold',
fontSize: getTokenValue('$6'),
}, },
}} }}
/> />

View File

@@ -3,7 +3,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { PlaylistScreen } from '../Playlist' import { PlaylistScreen } from '../Playlist'
import { Home as HomeComponent } from '../../components/Home' import { Home as HomeComponent } from '../../components/Home'
import { ArtistScreen } from '../Artist' import { ArtistScreen } from '../Artist'
import { useTheme } from 'tamagui' import { getTokenValue, useTheme } from 'tamagui'
import HomeArtistsScreen from './artists' import HomeArtistsScreen from './artists'
import HomeTracksScreen from './tracks' import HomeTracksScreen from './tracks'
import AlbumScreen from '../Album' import AlbumScreen from '../Album'
@@ -28,8 +28,10 @@ export default function Home(): React.JSX.Element {
component={HomeComponent} component={HomeComponent}
options={{ options={{
title: 'Home', title: 'Home',
headerTitleAlign: 'center',
headerTitleStyle: { headerTitleStyle: {
fontFamily: 'Figtree-Bold', fontFamily: 'Figtree-Bold',
fontSize: getTokenValue('$6'),
}, },
}} }}
/> />

View File

@@ -2,7 +2,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { ArtistScreen } from '../Artist' import { ArtistScreen } from '../Artist'
import AlbumScreen from '../Album' import AlbumScreen from '../Album'
import { PlaylistScreen } from '../Playlist' import { PlaylistScreen } from '../Playlist'
import { useTheme } from 'tamagui' import { getTokenValue, useTheme } from 'tamagui'
import Search from '../../components/Search' import Search from '../../components/Search'
import SearchParamList from './types' import SearchParamList from './types'
import InstantMix from '../../components/InstantMix/component' import InstantMix from '../../components/InstantMix/component'
@@ -20,8 +20,10 @@ export default function SearchStack(): React.JSX.Element {
component={Search} component={Search}
options={{ options={{
title: 'Search', title: 'Search',
headerTitleAlign: 'center',
headerTitleStyle: { headerTitleStyle: {
fontFamily: 'Figtree-Bold', fontFamily: 'Figtree-Bold',
fontSize: getTokenValue('$6'),
}, },
}} }}
/> />