mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-20 13:00:10 -06:00
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:
@@ -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,100 +99,135 @@ export default function LibrarySelector({
|
|||||||
}
|
}
|
||||||
}, [isPending, isSuccess, libraries])
|
}, [isPending, isSuccess, libraries])
|
||||||
|
|
||||||
const libraryToggleItems = useMemo(
|
const libraryToggleItems = musicLibraries.map((library) => {
|
||||||
() =>
|
const isSelected: boolean = selectedLibraryId === library.Id!
|
||||||
musicLibraries.map((library) => {
|
|
||||||
const isSelected: boolean = selectedLibraryId === library.Id!
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleGroup.Item
|
<ToggleGroup.Item
|
||||||
key={library.Id}
|
key={library.Id}
|
||||||
value={library.Id!}
|
value={library.Id!}
|
||||||
aria-label={library.Name!}
|
aria-label={library.Name!}
|
||||||
pressStyle={{
|
pressStyle={{
|
||||||
scale: 0.9,
|
scale: 0.9,
|
||||||
}}
|
}}
|
||||||
backgroundColor={isSelected ? '$primary' : '$background'}
|
backgroundColor={isSelected ? '$primary' : '$background'}
|
||||||
>
|
borderWidth={hasMultipleLibraries ? 1 : 0}
|
||||||
<Text
|
borderColor={isSelected ? '$primary' : '$borderColor'}
|
||||||
fontWeight={isSelected ? 'bold' : '600'}
|
>
|
||||||
color={isSelected ? '$background' : '$neutral'}
|
<Text
|
||||||
>
|
fontWeight={isSelected ? 'bold' : '600'}
|
||||||
{library.Name ?? 'Unnamed Library'}
|
color={isSelected ? '$background' : '$neutral'}
|
||||||
</Text>
|
>
|
||||||
</ToggleGroup.Item>
|
{library.Name ?? 'Unnamed Library'}
|
||||||
)
|
</Text>
|
||||||
}),
|
</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' : '$4'}
|
||||||
marginBottom={isOnboarding ? '$20' : 'unset'}
|
>
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInUp.springify()}
|
||||||
|
exiting={FadeOutUp.springify()}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<YStack flex={1} alignItems='center' justifyContent='flex-end'>
|
<H3 textAlign='center' marginBottom={'$2'}>
|
||||||
<H2 textAlign='center' marginBottom={'$2'}>
|
{title}
|
||||||
{title}
|
</H3>
|
||||||
</H2>
|
</Animated.View>
|
||||||
{!hasMultipleLibraries && !isOnboarding && (
|
{!hasMultipleLibraries && !isOnboarding && (
|
||||||
<Text color='$borderColor' textAlign='center'>
|
<Animated.View entering={FadeIn.springify()} exiting={FadeOut.springify()}>
|
||||||
Only one music library is available
|
<Text color='$borderColor' textAlign='center'>
|
||||||
</Text>
|
Only one music library is available
|
||||||
)}
|
</Text>
|
||||||
</YStack>
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
|
||||||
<YStack justifyContent='center' flexGrow={1} minHeight={'$12'} gap={'$4'}>
|
<Animated.View
|
||||||
{isPending ? (
|
style={{
|
||||||
<Spinner size='large' />
|
justifyContent: 'center',
|
||||||
) : isError ? (
|
flexGrow: 1,
|
||||||
<Text color='$danger' textAlign='center'>
|
}}
|
||||||
Unable to load libraries
|
>
|
||||||
</Text>
|
{isPending ? (
|
||||||
) : (
|
<Spinner size='large' enterStyle={{ opacity: 1 }} exitStyle={{ opacity: 0 }} />
|
||||||
<ToggleGroup
|
) : isError ? (
|
||||||
orientation='vertical'
|
<LoadErrorMessage />
|
||||||
type='single'
|
) : musicLibraries.length === 0 ? (
|
||||||
animation={'quick'}
|
<NoLibrariesMessage />
|
||||||
disableDeactivation={true}
|
) : (
|
||||||
value={selectedLibraryId}
|
<ToggleGroup
|
||||||
onValueChange={setSelectedLibraryId}
|
enterStyle={{ opacity: 1 }}
|
||||||
disabled={!hasMultipleLibraries && !isOnboarding}
|
exitStyle={{ opacity: 0 }}
|
||||||
>
|
orientation='vertical'
|
||||||
{libraryToggleItems}
|
type='single'
|
||||||
</ToggleGroup>
|
animation={'quick'}
|
||||||
)}
|
disableDeactivation={true}
|
||||||
</YStack>
|
value={selectedLibraryId}
|
||||||
|
onValueChange={setSelectedLibraryId}
|
||||||
<XStack alignItems='flex-end' gap={'$3'} marginTop={'$4'}>
|
disabled={!hasMultipleLibraries && !isOnboarding}
|
||||||
{showCancelButton && (
|
>
|
||||||
<Button
|
{libraryToggleItems}
|
||||||
variant='outlined'
|
</ToggleGroup>
|
||||||
icon={() => <Icon name={cancelButtonIcon} small />}
|
)}
|
||||||
onPress={onCancel}
|
</Animated.View>
|
||||||
flex={1}
|
|
||||||
>
|
|
||||||
{cancelButtonText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
<XStack alignItems='flex-end' gap={'$3'} marginTop={'$4'}>
|
||||||
|
{showCancelButton && (
|
||||||
<Button
|
<Button
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
borderColor={'$primary'}
|
icon={() => <Icon name={cancelButtonIcon} small />}
|
||||||
color={'$primary'}
|
onPress={onCancel}
|
||||||
disabled={!selectedLibraryId}
|
|
||||||
icon={() => <Icon name={primaryButtonIcon} small color='$primary' />}
|
|
||||||
onPress={handleLibrarySelection}
|
|
||||||
testID='let_s_go_button'
|
|
||||||
flex={1}
|
flex={1}
|
||||||
>
|
>
|
||||||
{primaryButtonText}
|
{cancelButtonText}
|
||||||
</Button>
|
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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={
|
||||||
<Text textAlign='center'>
|
<YStack justifyContent='center' alignContent='center'>
|
||||||
Wake now, discover that you are the eyes of the world...
|
<Text textAlign='center'>
|
||||||
</Text>
|
Wake now, discover that you are the eyes of the world...
|
||||||
|
</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,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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'),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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'),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user