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 { Spinner, ToggleGroup, XStack, YStack } from 'tamagui'
import React, { useEffect, useRef, useState } from 'react'
import { H3, Spinner, ToggleGroup, XStack, YStack } from 'tamagui'
import { H2, Text } from '../helpers/text'
import Button from '../helpers/button'
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 Icon from './icon'
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
import Animated, {
FadeIn,
FadeInUp,
FadeOut,
FadeOutUp,
LinearTransition,
} from 'react-native-reanimated'
interface LibrarySelectorProps {
onLibrarySelected: (
@@ -73,9 +80,15 @@ export default function LibrarySelector({
useEffect(() => {
if (!isPending && isSuccess && libraries) {
setMusicLibraries(
libraries.filter((library) => library.CollectionType === CollectionType.Music),
const filteredMusicLibraries = libraries.filter(
(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
const foundPlaylistLibrary = libraries.find(
@@ -86,100 +99,135 @@ export default function LibrarySelector({
}
}, [isPending, isSuccess, libraries])
const libraryToggleItems = useMemo(
() =>
musicLibraries.map((library) => {
const isSelected: boolean = selectedLibraryId === library.Id!
const libraryToggleItems = musicLibraries.map((library) => {
const isSelected: boolean = selectedLibraryId === library.Id!
return (
<ToggleGroup.Item
key={library.Id}
value={library.Id!}
aria-label={library.Name!}
pressStyle={{
scale: 0.9,
}}
backgroundColor={isSelected ? '$primary' : '$background'}
>
<Text
fontWeight={isSelected ? 'bold' : '600'}
color={isSelected ? '$background' : '$neutral'}
>
{library.Name ?? 'Unnamed Library'}
</Text>
</ToggleGroup.Item>
)
}),
[selectedLibraryId, musicLibraries],
)
return (
<ToggleGroup.Item
key={library.Id}
value={library.Id!}
aria-label={library.Name!}
pressStyle={{
scale: 0.9,
}}
backgroundColor={isSelected ? '$primary' : '$background'}
borderWidth={hasMultipleLibraries ? 1 : 0}
borderColor={isSelected ? '$primary' : '$borderColor'}
>
<Text
fontWeight={isSelected ? 'bold' : '600'}
color={isSelected ? '$background' : '$neutral'}
>
{library.Name ?? 'Unnamed Library'}
</Text>
</ToggleGroup.Item>
)
})
return (
<SafeAreaView style={{ flex: 1 }}>
<YStack
flex={1}
justifyContent='center'
paddingHorizontal={'$4'}
marginBottom={isOnboarding ? '$20' : 'unset'}
<YStack
flex={1}
justifyContent='center'
paddingHorizontal={'$4'}
marginBottom={isOnboarding ? '$20' : '$4'}
>
<Animated.View
entering={FadeInUp.springify()}
exiting={FadeOutUp.springify()}
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'flex-end',
}}
>
<YStack flex={1} alignItems='center' justifyContent='flex-end'>
<H2 textAlign='center' marginBottom={'$2'}>
{title}
</H2>
{!hasMultipleLibraries && !isOnboarding && (
<Text color='$borderColor' textAlign='center'>
Only one music library is available
</Text>
)}
</YStack>
<H3 textAlign='center' marginBottom={'$2'}>
{title}
</H3>
</Animated.View>
{!hasMultipleLibraries && !isOnboarding && (
<Animated.View entering={FadeIn.springify()} exiting={FadeOut.springify()}>
<Text color='$borderColor' textAlign='center'>
Only one music library is available
</Text>
</Animated.View>
)}
<YStack justifyContent='center' flexGrow={1} minHeight={'$12'} gap={'$4'}>
{isPending ? (
<Spinner size='large' />
) : isError ? (
<Text color='$danger' textAlign='center'>
Unable to load libraries
</Text>
) : (
<ToggleGroup
orientation='vertical'
type='single'
animation={'quick'}
disableDeactivation={true}
value={selectedLibraryId}
onValueChange={setSelectedLibraryId}
disabled={!hasMultipleLibraries && !isOnboarding}
>
{libraryToggleItems}
</ToggleGroup>
)}
</YStack>
<XStack alignItems='flex-end' gap={'$3'} marginTop={'$4'}>
{showCancelButton && (
<Button
variant='outlined'
icon={() => <Icon name={cancelButtonIcon} small />}
onPress={onCancel}
flex={1}
>
{cancelButtonText}
</Button>
)}
<Animated.View
style={{
justifyContent: 'center',
flexGrow: 1,
}}
>
{isPending ? (
<Spinner size='large' enterStyle={{ opacity: 1 }} exitStyle={{ opacity: 0 }} />
) : isError ? (
<LoadErrorMessage />
) : musicLibraries.length === 0 ? (
<NoLibrariesMessage />
) : (
<ToggleGroup
enterStyle={{ opacity: 1 }}
exitStyle={{ opacity: 0 }}
orientation='vertical'
type='single'
animation={'quick'}
disableDeactivation={true}
value={selectedLibraryId}
onValueChange={setSelectedLibraryId}
disabled={!hasMultipleLibraries && !isOnboarding}
>
{libraryToggleItems}
</ToggleGroup>
)}
</Animated.View>
<XStack alignItems='flex-end' gap={'$3'} marginTop={'$4'}>
{showCancelButton && (
<Button
variant='outlined'
borderColor={'$primary'}
color={'$primary'}
disabled={!selectedLibraryId}
icon={() => <Icon name={primaryButtonIcon} small color='$primary' />}
onPress={handleLibrarySelection}
testID='let_s_go_button'
icon={() => <Icon name={cancelButtonIcon} small />}
onPress={onCancel}
flex={1}
>
{primaryButtonText}
{cancelButtonText}
</Button>
</XStack>
</YStack>
</SafeAreaView>
)}
<Button
variant='outlined'
borderColor={'$primary'}
color={'$primary'}
disabled={!selectedLibraryId}
icon={() => <Icon name={primaryButtonIcon} small color='$primary' />}
onPress={handleLibrarySelection}
testID='let_s_go_button'
flex={1}
>
{primaryButtonText}
</Button>
</XStack>
</YStack>
)
}
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'
onChangeText={(value) => handleSearchStringUpdate(value)}
value={searchString}
marginHorizontal={'$2'}
testID='search-input'
clearButtonMode='while-editing'
/>
@@ -149,7 +148,7 @@ export default function Search({
renderItem={({ item }) => <ItemRow item={item} navigation={navigation} />}
onScrollBeginDrag={handleScrollBeginDrag}
style={{
marginHorizontal: getToken('$2'),
marginHorizontal: getToken('$4'),
marginTop: getToken('$4'),
}}
/>

View File

@@ -1,6 +1,6 @@
import ItemRow from '../Global/components/item-row'
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 HorizontalCardList from '../Global/components/horizontal-list'
import { FlashList } from '@shopify/flash-list'
@@ -26,7 +26,7 @@ export default function Suggestions({
data={suggestions?.filter((suggestion) => suggestion.Type !== 'MusicArtist')}
ListHeaderComponent={
<YStack>
<H3>Suggestions</H3>
<H5>Suggestions</H5>
<HorizontalCardList
data={suggestions?.filter(
@@ -51,17 +51,17 @@ export default function Suggestions({
}
ItemSeparatorComponent={() => <Separator />}
ListEmptyComponent={
<Text textAlign='center'>
Wake now, discover that you are the eyes of the world...
</Text>
<YStack justifyContent='center' alignContent='center'>
<Text textAlign='center'>
Wake now, discover that you are the eyes of the world...
</Text>
<Spinner color={'$primary'} />
</YStack>
}
onScrollBeginDrag={handleScrollBeginDrag}
renderItem={({ item }) => {
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 AlbumScreen from '../Album'
import { ArtistScreen } from '../Artist'
import { useTheme } from 'tamagui'
import { getTokenValue, useTheme } from 'tamagui'
import RecentlyAdded from './albums'
import PublicPlaylists from './playlists'
import { PlaylistScreen } from '../Playlist'
@@ -22,8 +22,10 @@ export function Discover(): React.JSX.Element {
name='Discover'
component={Index}
options={{
headerTitleAlign: 'center',
headerTitleStyle: {
fontFamily: 'Figtree-Bold',
fontSize: getTokenValue('$6'),
},
}}
/>

View File

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

View File

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