mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-06 02:50:30 -06:00
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:
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function Icon({
|
||||
|
||||
return (
|
||||
<YStack
|
||||
alignContent='flex-start'
|
||||
alignContent='center'
|
||||
justifyContent='center'
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user