styling fixes

make buttons animate consistently

styling and animations for the library switcher

fix issue where the second line of the toast message wasn't using the figtree font
This commit is contained in:
Violet Caulfield
2025-11-08 02:46:01 -06:00
parent e026f76f98
commit 425a100ed9
12 changed files with 165 additions and 115 deletions

View File

@@ -9,7 +9,7 @@ import { useApi, useJellifyUser } from '../../../stores'
interface AuthenticateUserByNameMutation {
onSuccess?: () => void
onError?: () => void
onError?: (error: Error) => void
}
const useAuthenticateUserByName = ({ onSuccess, onError }: AuthenticateUserByNameMutation) => {
@@ -51,7 +51,7 @@ const useAuthenticateUserByName = ({ onSuccess, onError }: AuthenticateUserByNam
onError: async (error: Error) => {
console.error('An error occurred connecting to the Jellyfin instance', error)
if (onError) onError()
if (onError) onError(error)
},
retry: 0,
gcTime: 0,

View File

@@ -3,7 +3,18 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { addManyToPlaylist, addToPlaylist } from '../../api/mutations/playlists'
import { useState } from 'react'
import Toast from 'react-native-toast-message'
import { YStack, XStack, Spacer, YGroup, Separator, ListItem, getTokens, ScrollView } from 'tamagui'
import {
YStack,
XStack,
Spacer,
YGroup,
Separator,
ListItem,
getTokens,
ScrollView,
useTheme,
Spinner,
} from 'tamagui'
import Icon from '../Global/components/icon'
import { AddToPlaylistMutation } from './types'
import { Text } from '../Global/helpers/text'
@@ -16,6 +27,7 @@ import { usePlaylistTracks, useUserPlaylists } from '../../api/queries/playlist'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useApi, useJellifyUser } from '../../stores'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import JellifyToastConfig from '../../configs/toast.config'
export default function AddToPlaylist({
track,
@@ -28,6 +40,8 @@ export default function AddToPlaylist({
}): React.JSX.Element {
const { bottom } = useSafeAreaInsets()
const theme = useTheme()
const {
data: playlists,
isPending: playlistsFetchPending,
@@ -69,6 +83,12 @@ export default function AddToPlaylist({
))}
</YGroup>
)}
<Toast
position='bottom'
bottomOffset={bottom * 2.5}
config={JellifyToastConfig(theme)}
/>
</ScrollView>
)
}
@@ -133,8 +153,8 @@ function AddToPlaylistRow({
animation={'quick'}
disabled={isInPlaylist}
hoverTheme
opacity={isInPlaylist ? 0.7 : 1}
pressStyle={{ opacity: 0.5 }}
opacity={isInPlaylist ? 0.5 : 1}
pressStyle={{ opacity: 0.6 }}
onPress={() => {
if (!isInPlaylist) {
useAddToPlaylist.mutate({
@@ -159,6 +179,8 @@ function AddToPlaylistRow({
<Animated.View entering={FadeIn} exiting={FadeOut}>
{isInPlaylist ? (
<Icon flex={1} name='check-circle-outline' color={'$success'} />
) : fetchingPlaylistTracks ? (
<Spinner color={'$primary'} />
) : (
<Spacer flex={1} />
)}

View File

@@ -109,45 +109,40 @@ export default function ItemContext({
return (
// Tons of padding top for iOS on the scrollview otherwise the context sheet header overlaps the content
<ScrollView
paddingTop={Platform.OS === 'ios' ? '$10' : undefined}
paddingBottom={Platform.OS === 'android' ? '$10' : undefined}
>
<YGroup unstyled>
<FavoriteContextMenuRow item={item} />
<YGroup unstyled>
<FavoriteContextMenuRow item={item} />
{renderAddToQueueRow && <AddToQueueMenuRow tracks={itemTracks} />}
{renderAddToQueueRow && <AddToQueueMenuRow tracks={itemTracks} />}
{renderAddToQueueRow && <DownloadMenuRow items={itemTracks} />}
{renderAddToQueueRow && <DownloadMenuRow items={itemTracks} />}
{renderAddToPlaylistRow && (
<AddToPlaylistRow
track={isTrack ? item : undefined}
tracks={isAlbum && discs ? discs.flatMap((d) => d.data) : undefined}
source={isAlbum ? item : undefined}
/>
)}
{renderAddToPlaylistRow && (
<AddToPlaylistRow
track={isTrack ? item : undefined}
tracks={isAlbum && discs ? discs.flatMap((d) => d.data) : undefined}
source={isAlbum ? item : undefined}
/>
)}
{(streamingMediaSourceInfo || downloadedMediaSourceInfo) && (
<StatsRow
item={item}
streamingMediaSourceInfo={streamingMediaSourceInfo}
downloadedMediaSourceInfo={downloadedMediaSourceInfo}
/>
)}
{(streamingMediaSourceInfo || downloadedMediaSourceInfo) && (
<StatsRow
item={item}
streamingMediaSourceInfo={streamingMediaSourceInfo}
downloadedMediaSourceInfo={downloadedMediaSourceInfo}
/>
)}
{renderViewAlbumRow && (
<ViewAlbumMenuRow
album={isAlbum ? item : album!}
stackNavigation={stackNavigation}
/>
)}
{renderViewAlbumRow && (
<ViewAlbumMenuRow
album={isAlbum ? item : album!}
stackNavigation={stackNavigation}
/>
)}
{!isPlaylist && (
<ArtistMenuRows artistIds={artistIds} stackNavigation={stackNavigation} />
)}
</YGroup>
</ScrollView>
{!isPlaylist && (
<ArtistMenuRows artistIds={artistIds} stackNavigation={stackNavigation} />
)}
</YGroup>
)
}

View File

@@ -44,7 +44,7 @@ export default function Icon({
return (
<YStack
alignContent='flex-start'
alignContent='center'
justifyContent='center'
onPress={onPress}
onPressIn={onPressIn}

View File

@@ -1,14 +1,13 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { isUndefined } from 'lodash'
import { getTokenValue, Token, View } from 'tamagui'
import { getTokenValue, Token, View, Image as TamaguiImage, ZStack } from 'tamagui'
import { StyleSheet } from 'react-native'
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { NitroImage, useImage } from 'react-native-nitro-image'
import { Blurhash } from 'react-native-blurhash'
import { getBlurhashFromDto } from '../../../utils/blurhash'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import { getItemImageUrl } from '../../../api/queries/image/utils'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { useApi } from '../../../stores'
interface ItemImageProps {
@@ -91,7 +90,7 @@ interface ImageProps {
testID?: string | undefined
}
const AnimatedNitroImage = Animated.createAnimatedComponent(NitroImage)
const AnimatedTamaguiImage = Animated.createAnimatedComponent(TamaguiImage)
function Image({
item,
@@ -102,7 +101,7 @@ function Image({
cornered,
testID,
}: ImageProps): React.JSX.Element {
const { image } = useImage({ url: imageUrl })
const [isLoaded, setIsLoaded] = useState<boolean>(false)
const imageViewStyle = useMemo(
() =>
@@ -137,21 +136,24 @@ function Image({
)
return (
<View style={imageViewStyle.view} justifyContent='center' alignContent='center'>
{image ? (
<AnimatedNitroImage
resizeMode='cover'
recyclingKey={imageUrl}
image={image}
testID={testID}
entering={FadeIn}
exiting={FadeOut}
style={Styles.blurhash}
/>
) : (
<ItemBlurhash item={item} />
)}
</View>
<ZStack style={imageViewStyle.view} justifyContent='center' alignContent='center'>
(!isLoaded && (
<ItemBlurhash item={item} />
))
<AnimatedTamaguiImage
objectFit='cover'
// recyclingKey={imageUrl}
source={{
uri: imageUrl,
cache: 'default',
}}
onLoad={() => setIsLoaded(true)}
testID={testID}
entering={FadeIn}
exiting={FadeOut}
style={Styles.blurhash}
/>
</ZStack>
)
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'
import { getToken, Spinner, ToggleGroup, YStack } from 'tamagui'
import React, { useEffect, useMemo, useState } from 'react'
import { 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'
@@ -85,10 +85,37 @@ export default function LibrarySelector({
}
}, [isPending, isSuccess, libraries])
const libraryToggleItems = useMemo(
() =>
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' : '$color'}
>
{library.Name ?? 'Unnamed Library'}
</Text>
</ToggleGroup.Item>
)
}),
[selectedLibraryId, musicLibraries],
)
return (
<SafeAreaView style={{ flex: 1 }}>
<YStack flex={1} justifyContent='center' paddingHorizontal={'$4'}>
<YStack alignItems='center' marginBottom={'$6'}>
<YStack flex={1} alignItems='center' justifyContent='flex-end'>
<H2 textAlign='center' marginBottom={'$2'}>
{title}
</H2>
@@ -99,7 +126,7 @@ export default function LibrarySelector({
)}
</YStack>
<YStack gap={'$4'}>
<YStack justifyContent='center' flexGrow={1} minHeight={'$12'} gap={'$4'}>
{isPending ? (
<Spinner size='large' />
) : isError ? (
@@ -110,50 +137,42 @@ export default function LibrarySelector({
<ToggleGroup
orientation='vertical'
type='single'
animation={'quick'}
disableDeactivation={true}
value={selectedLibraryId}
onValueChange={setSelectedLibraryId}
disabled={!hasMultipleLibraries && !isOnboarding}
>
{musicLibraries.map((library) => (
<ToggleGroup.Item
key={library.Id}
value={library.Id!}
aria-label={library.Name!}
backgroundColor={
selectedLibraryId === library.Id
? getToken('$color.purpleGray')
: 'unset'
}
opacity={!hasMultipleLibraries && !isOnboarding ? 0.6 : 1}
>
<Text>{library.Name ?? 'Unnamed Library'}</Text>
</ToggleGroup.Item>
))}
{libraryToggleItems}
</ToggleGroup>
)}
<YStack gap={'$3'} marginTop={'$4'}>
<Button
disabled={!selectedLibraryId}
icon={() => <Icon name={primaryButtonIcon} small />}
onPress={handleLibrarySelection}
testID='let_s_go_button'
>
{primaryButtonText}
</Button>
{showCancelButton && (
<Button
variant='outlined'
icon={() => <Icon name={cancelButtonIcon} small />}
onPress={onCancel}
>
{cancelButtonText}
</Button>
)}
</YStack>
</YStack>
<XStack alignItems='flex-end' gap={'$3'} marginTop={'$4'}>
{showCancelButton && (
<Button
variant='outlined'
icon={() => <Icon name={cancelButtonIcon} small />}
onPress={onCancel}
flex={1}
>
{cancelButtonText}
</Button>
)}
<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>
</SafeAreaView>
)

View File

@@ -8,5 +8,15 @@ interface ButtonProps extends TamaguiButtonProps {
}
export default function Button(props: ButtonProps): React.JSX.Element {
return <TamaguiButton opacity={props.disabled ? 0.5 : 1} marginVertical={'$2'} {...props} />
return (
<TamaguiButton
opacity={props.disabled ? 0.5 : 1}
animation={'quick'}
pressStyle={{
scale: 0.9,
}}
{...props}
marginVertical={'$2'}
/>
)
}

View File

@@ -4,7 +4,7 @@ import { YStack, useTheme, ZStack, useWindowDimensions, View, getTokenValue } fr
import Scrubber from './components/scrubber'
import Controls from './components/controls'
import Toast from 'react-native-toast-message'
import JellifyToastConfig from '../../constants/toast.config'
import JellifyToastConfig from '../../configs/toast.config'
import { useFocusEffect } from '@react-navigation/native'
import Footer from './components/footer'
import BlurredBackground from './components/blurred-background'

View File

@@ -34,9 +34,12 @@ export default function PreferencesTab(): React.JSX.Element {
onPress: () => void
}) => (
<Button
pressStyle={{
backgroundColor: '$neutral',
}}
onPress={onPress}
backgroundColor={active ? '$primary' : 'transparent'}
borderColor={active ? '$primary' : '$borderColor'}
backgroundColor={active ? '$success' : 'transparent'}
borderColor={active ? '$success' : '$borderColor'}
borderWidth={'$0.5'}
color={active ? '$background' : '$color'}
paddingHorizontal={'$3'}

View File

@@ -14,7 +14,7 @@ import glitchtipConfig from '../../glitchtip.json'
import * as Sentry from '@sentry/react-native'
import { getToken, Theme, useTheme } from 'tamagui'
import Toast from 'react-native-toast-message'
import JellifyToastConfig from '../constants/toast.config'
import JellifyToastConfig from '../configs/toast.config'
import { useColorScheme } from 'react-native'
import { CarPlayProvider } from '../providers/CarPlay'
import { useSelectPlayerEngine } from '../stores/player/engine'

View File

@@ -1,6 +1,6 @@
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'
import { BaseToast, BaseToastProps, ToastConfig } from 'react-native-toast-message'
import { getToken, getTokenValue, ThemeParsed } from 'tamagui'
import { ThemeParsed } from 'tamagui'
import Icon from '../components/Global/components/icon'
/**
* Configures the toast for the Jellify app, using Tamagui style tokens
@@ -19,6 +19,10 @@ const JellifyToastConfig: (theme: ThemeParsed) => ToastConfig = (theme: ThemePar
fontFamily: 'Figtree-Bold',
color: theme.color.val,
},
text2Style: {
fontFamily: 'Figtree-Bold',
color: theme.neutral.val,
},
}),
error: (props: BaseToastProps) =>
BaseToast({
@@ -31,6 +35,10 @@ const JellifyToastConfig: (theme: ThemeParsed) => ToastConfig = (theme: ThemePar
fontFamily: 'Figtree-Bold',
color: theme.color.val,
},
text2Style: {
fontFamily: 'Figtree-Bold',
color: theme.neutral.val,
},
}),
})