fresh styling in the library and settings

Adds more purple colors to the library and settings tabs

update animations for the a-z selector in the library

make refresh indicators

consistly colored across the app

fixes some styling around gesture handling

update tamagui to 1.136.6
This commit is contained in:
Violet Caulfield
2025-11-07 23:09:57 -06:00
parent 0dcec07f18
commit f011fa34ef
21 changed files with 1174 additions and 1045 deletions

View File

@@ -47,7 +47,7 @@
"@react-navigation/native-stack": "7.6.1",
"@sentry/react-native": "7.4.0",
"@shopify/flash-list": "2.2.0",
"@tamagui/config": "1.135.4",
"@tamagui/config": "1.136.6",
"@tanstack/query-async-storage-persister": "5.89.0",
"@tanstack/react-query": "5.89.0",
"@tanstack/react-query-persist-client": "5.89.0",
@@ -94,7 +94,7 @@
"react-native-worklets": "0.6.1",
"ruby": "^0.6.1",
"scheduler": "^0.26.0",
"tamagui": "1.135.4",
"tamagui": "1.136.6",
"zustand": "^5.0.8"
},
"devDependencies": {

View File

@@ -1,5 +1,5 @@
import { ActivityIndicator, RefreshControl } from 'react-native'
import { getToken, Separator, XStack, YStack } from 'tamagui'
import { Separator, useTheme, XStack, YStack } from 'tamagui'
import React, { RefObject, useEffect, useRef } from 'react'
import { Text } from '../Global/helpers/text'
import { FlashList, FlashListRef } from '@shopify/flash-list'
@@ -11,6 +11,7 @@ import LibraryStackParamList from '../../screens/Library/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector'
import { isString } from 'lodash'
import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header'
interface AlbumsProps {
albumsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
@@ -23,6 +24,8 @@ export default function Albums({
albumPageParams,
showAlphabeticalSelector,
}: AlbumsProps): React.JSX.Element {
const theme = useTheme()
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
const sectionListRef = useRef<FlashListRef<string | number | BaseItemDto>>(null)
@@ -94,18 +97,7 @@ export default function Albums({
}
renderItem={({ index, item: album }) =>
typeof album === 'string' ? (
<XStack
padding={'$2'}
backgroundColor={'$background'}
borderRadius={'$5'}
borderWidth={'$1'}
borderColor={'$primary'}
marginRight={'$2'}
>
<Text bold color={'$primary'}>
{album.toUpperCase()}
</Text>
</XStack>
<FlashListStickyHeader text={album.toUpperCase()} />
) : typeof album === 'number' ? null : typeof album === 'object' ? (
<ItemRow item={album} navigation={navigation} />
) : null
@@ -123,11 +115,12 @@ export default function Albums({
ListFooterComponent={
albumsInfiniteQuery.isFetchingNextPage ? <ActivityIndicator /> : null
}
ItemSeparatorComponent={() => <Separator />}
ItemSeparatorComponent={() => <Separator borderColor={'$neutral'} />}
refreshControl={
<RefreshControl
refreshing={albumsInfiniteQuery.isFetching}
onRefresh={albumsInfiniteQuery.refetch}
tintColor={theme.primary.val}
/>
}
stickyHeaderIndices={stickyHeaderIndices}

View File

@@ -12,6 +12,7 @@ import { isString } from 'lodash'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import LibraryStackParamList from '../../screens/Library/types'
import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header'
export interface ArtistsProps {
artistsInfiniteQuery: UseInfiniteQueryResult<
@@ -128,18 +129,7 @@ export default function Artists({
// If the index is the last index, or the next index is not an object, then don't render the letter
index - 1 === artists.length ||
typeof artists[index + 1] !== 'object' ? null : (
<XStack
padding={'$2'}
backgroundColor={'$background'}
borderRadius={'$4'}
borderWidth={'$1'}
borderColor={'$primary'}
marginRight={'$2'}
>
<Text bold color={'$primary'}>
{artist.toUpperCase()}
</Text>
</XStack>
<FlashListStickyHeader text={artist.toUpperCase()} />
)
) : typeof artist === 'number' ? null : typeof artist === 'object' ? (
<ItemRow circular item={artist} navigation={navigation} />

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { getToken, ScrollView, View, YStack } from 'tamagui'
import { getToken, ScrollView, useTheme, View, YStack } from 'tamagui'
import RecentlyAdded from './helpers/just-added'
import { useDiscoverContext } from '../../providers/Discover'
import { RefreshControl } from 'react-native'
@@ -7,6 +7,8 @@ import PublicPlaylists from './helpers/public-playlists'
import SuggestedArtists from './helpers/suggested-artists'
export default function Index(): React.JSX.Element {
const theme = useTheme()
const { refreshing, refresh, publicPlaylists, suggestedArtistsInfiniteQuery } =
useDiscoverContext()
@@ -19,7 +21,13 @@ export default function Index(): React.JSX.Element {
}}
contentInsetAdjustmentBehavior='automatic'
removeClippedSubviews
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={refresh}
tintColor={theme.primary.val}
/>
}
>
<YStack gap={'$3'}>
<View testID='discover-recently-added'>

View File

@@ -2,7 +2,13 @@ import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'
import { LayoutChangeEvent, View as RNView } from 'react-native'
import { getToken, useTheme, View, YStack } from 'tamagui'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
withSpring,
} from 'react-native-reanimated'
import { runOnJS } from 'react-native-worklets'
import { Text } from '../helpers/text'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
@@ -44,12 +50,21 @@ export default function AZScroller({
const showOverlay = () => {
'worklet'
overlayOpacity.value = withTiming(1)
overlayOpacity.value = withSpring(1)
}
const hideOverlay = () => {
'worklet'
overlayOpacity.value = withTiming(0)
overlayOpacity.value = withSpring(0)
}
const setOverlayPositionY = (y: number) => {
'worket'
gesturePositionY.value = withSpring(y, {
mass: 4,
damping: 120,
stiffness: 1050,
})
}
const panGesture = useMemo(
@@ -58,7 +73,7 @@ export default function AZScroller({
.runOnJS(true)
.onBegin((e) => {
const relativeY = e.absoluteY - alphabetSelectorTopY.current
gesturePositionY.set(relativeY)
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
const index = Math.floor(relativeY / letterHeight.current)
if (alphabet[index]) {
const letter = alphabet[index]
@@ -69,7 +84,7 @@ export default function AZScroller({
})
.onUpdate((e) => {
const relativeY = e.absoluteY - alphabetSelectorTopY.current
gesturePositionY.set(relativeY)
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
const index = Math.floor(relativeY / letterHeight.current)
if (alphabet[index]) {
const letter = alphabet[index]
@@ -93,7 +108,7 @@ export default function AZScroller({
.runOnJS(true)
.onBegin((e) => {
const relativeY = e.absoluteY - alphabetSelectorTopY.current
gesturePositionY.set(relativeY)
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
const index = Math.floor(relativeY / letterHeight.current)
if (alphabet[index]) {
const letter = alphabet[index]
@@ -179,10 +194,8 @@ export default function AZScroller({
width: getToken('$13'),
height: getToken('$13'),
justifyContent: 'center',
backgroundColor: theme.background.val,
backgroundColor: theme.primary.val,
borderRadius: getToken('$4'),
borderWidth: getToken('$1'),
borderColor: theme.primary.val,
},
animatedOverlayStyle,
]}
@@ -192,7 +205,7 @@ export default function AZScroller({
fontSize: getToken('$12'),
textAlign: 'center',
fontFamily: 'Figtree-Bold',
color: theme.primary.val,
color: theme.background.val,
marginHorizontal: 'auto',
}}
>

View File

@@ -1,12 +1,13 @@
import React from 'react'
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'
import { CheckboxProps, XStack, Checkbox, Label } from 'tamagui'
import { CheckboxProps, XStack, Checkbox, Label, useTheme } from 'tamagui'
export function CheckboxWithLabel({
size,
label = 'Toggle',
...checkboxProps
}: CheckboxProps & { label?: string }) {
const theme = useTheme()
const id = `checkbox-${(size || '').toString().slice(1)}`
return (
<XStack width={150} alignItems='center' gap='$4'>
@@ -16,7 +17,7 @@ export function CheckboxWithLabel({
</Checkbox.Indicator>
</Checkbox>
<Label size={size} htmlFor={id}>
<Label color={theme.primary.val} size={size} htmlFor={id}>
{label}
</Label>
</XStack>

View File

@@ -0,0 +1,38 @@
import useIsLightMode from '@/src/hooks/use-is-light-mode'
import LinearGradient from 'react-native-linear-gradient'
import { Text, useTheme, XStack, ZStack } from 'tamagui'
export default function FlashListStickyHeader({ text }: { text: string }): React.JSX.Element {
const theme = useTheme()
return (
<ZStack padding={'$2'} paddingLeft={'$2'} backgroundColor={'$primary'} minHeight={'$2.5'}>
<XStack flex={1} alignItems='center' paddingLeft={'$2'}>
<Text marginRight={'$4'} fontSize={'$4'} fontWeight={'bold'} color={'$background'}>
{text}
</Text>
</XStack>
<LinearGradient
start={{
x: 0,
y: 0,
}}
end={{
x: 1,
y: 0,
}}
locations={[0.1, 0.9]}
colors={['transparent', theme.background.val]}
style={{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
flex: 1,
}}
/>
</ZStack>
)
}

View File

@@ -0,0 +1,21 @@
import useIsLightMode from '../../../hooks/use-is-light-mode'
import { useIsFocused } from '@react-navigation/native'
import { useMemo } from 'react'
import { StatusBar as RNStatusBar, StatusBarStyle } from 'react-native'
interface StatusBarProps {
invertColors?: boolean | undefined
}
export default function StatusBar({ invertColors }: StatusBarProps): React.JSX.Element | null {
const isFocused = useIsFocused()
const isLightMode = useIsLightMode()
const barStyle: StatusBarStyle = useMemo(
() => (isLightMode || (invertColors && !isLightMode) ? 'dark-content' : 'light-content'),
[invertColors, isLightMode],
)
return isFocused ? <RNStatusBar barStyle={barStyle} /> : null
}

View File

@@ -1,5 +1,5 @@
import { ScrollView, RefreshControl, Platform } from 'react-native'
import { YStack, getToken } from 'tamagui'
import { YStack, getToken, useTheme } from 'tamagui'
import RecentArtists from './helpers/recent-artists'
import RecentlyPlayed from './helpers/recently-played'
import FrequentArtists from './helpers/frequent-artists'
@@ -13,6 +13,8 @@ const COMPONENT_NAME = 'Home'
export function Home(): React.JSX.Element {
usePreventRemove(true, () => {})
const theme = useTheme()
usePerformanceMonitor(COMPONENT_NAME, 5)
const { isPending: refreshing, mutate: refresh } = useHomeQueries()
@@ -24,7 +26,13 @@ export function Home(): React.JSX.Element {
marginVertical: getToken('$4'),
marginHorizontal: getToken('$2'),
}}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={refresh}
tintColor={theme.primary.val}
/>
}
>
<YStack
alignContent='flex-start'

View File

@@ -25,11 +25,15 @@ export default function LibraryScreen({
tabBarItemStyle: {
height: getToken('$12') + getToken('$6'),
},
tabBarActiveTintColor: theme.primary.val,
tabBarInactiveTintColor: theme.neutral.val,
tabBarActiveTintColor: theme.background.val,
tabBarInactiveTintColor: theme.background50.val,
tabBarStyle: {
backgroundColor: theme.primary.val,
},
tabBarLabelStyle: {
fontFamily: 'Figtree-Bold',
},
tabBarPressOpacity: 0.5,
lazy: true, // Enable lazy loading to prevent all tabs from mounting simultaneously
}}
>
@@ -40,7 +44,7 @@ export default function LibraryScreen({
tabBarIcon: ({ focused, color }) => (
<Icon
name='microphone-variant'
color={focused ? '$primary' : '$neutral'}
color={focused ? '$background' : '$background50'}
small
/>
),
@@ -55,7 +59,7 @@ export default function LibraryScreen({
tabBarIcon: ({ focused, color }) => (
<Icon
name={`music-box-multiple${!focused ? '-outline' : ''}`}
color={focused ? '$primary' : '$neutral'}
color={focused ? '$background' : '$background50'}
small
/>
),
@@ -70,7 +74,7 @@ export default function LibraryScreen({
tabBarIcon: ({ focused, color }) => (
<Icon
name='music-clef-treble'
color={focused ? '$primary' : '$neutral'}
color={focused ? '$background' : '$background50'}
small
/>
),
@@ -85,7 +89,7 @@ export default function LibraryScreen({
tabBarIcon: ({ focused, color }) => (
<Icon
name='playlist-music'
color={focused ? '$primary' : '$neutral'}
color={focused ? '$background' : '$background50'}
small
/>
),

View File

@@ -1,12 +1,13 @@
import { MaterialTopTabBar, MaterialTopTabBarProps } from '@react-navigation/material-top-tabs'
import React from 'react'
import { XStack, YStack } from 'tamagui'
import { getTokenValue, Square, XStack, YStack } from 'tamagui'
import Icon from '../Global/components/icon'
import { useLibrarySortAndFilterContext } from '../../providers/Library'
import { Text } from '../Global/helpers/text'
import { isUndefined } from 'lodash'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import useHapticFeedback from '../../hooks/use-haptic-feedback'
import StatusBar from '../Global/helpers/status-bar'
function LibraryTabBar(props: MaterialTopTabBarProps) {
const { isFavorites, setIsFavorites, isDownloaded, setIsDownloaded } =
@@ -17,20 +18,26 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
const insets = useSafeAreaInsets()
return (
<YStack paddingTop={insets.top}>
<YStack>
<Square height={insets.top} backgroundColor={'$primary'} />
<StatusBar invertColors />
<MaterialTopTabBar {...props} />
{[''].includes(props.state.routes[props.state.index].name) ? null : (
<XStack
borderColor={'$borderColor'}
marginTop={'$2'}
backgroundColor={'$background'}
alignItems={'center'}
justifyContent='flex-start'
paddingHorizontal={'$4'}
paddingVertical={'$1'}
gap={'$4'}
maxWidth={'80%'}
shadowOffset={{
width: 0,
height: getTokenValue('$2'),
}}
shadowColor={'$background25'}
>
{props.state.routes[props.state.index].name === 'Playlists' ? (
<XStack

View File

@@ -1,4 +1,4 @@
import { Separator, XStack } from 'tamagui'
import { Separator, useTheme, XStack } from 'tamagui'
import Track from '../Global/components/track'
import Icon from '../Global/components/icon'
import { RefreshControl } from 'react-native'
@@ -31,6 +31,8 @@ export default function Playlist({
const trigger = useHapticFeedback()
const theme = useTheme()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const scrollOffsetHandler = useAnimatedScrollHandler({
@@ -46,7 +48,13 @@ export default function Playlist({
return (
<AnimatedDraggableFlatList
refreshControl={<RefreshControl refreshing={isPending} onRefresh={refetch} />}
refreshControl={
<RefreshControl
refreshing={isPending}
onRefresh={refetch}
tintColor={theme.primary.val}
/>
}
contentInsetAdjustmentBehavior='automatic'
data={playlistTracks ?? []}
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336

View File

@@ -1,5 +1,5 @@
import { RefreshControl } from 'react-native-gesture-handler'
import { Separator } from 'tamagui'
import { Separator, useTheme } from 'tamagui'
import { FlashList } from '@shopify/flash-list'
import ItemRow from '../Global/components/item-row'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
@@ -26,6 +26,8 @@ export default function Playlists({
isFetchingNextPage,
canEdit,
}: PlaylistsProps): React.JSX.Element {
const theme = useTheme()
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
return (
@@ -33,7 +35,11 @@ export default function Playlists({
contentInsetAdjustmentBehavior='automatic'
data={playlists}
refreshControl={
<RefreshControl refreshing={isPending || isFetchingNextPage} onRefresh={refetch} />
<RefreshControl
refreshing={isPending || isFetchingNextPage}
onRefresh={refetch}
tintColor={theme.primary.val}
/>
}
ItemSeparatorComponent={() => <Separator />}
renderItem={({ index, item: playlist }) => (

View File

@@ -1,12 +1,12 @@
import React from 'react'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import { getToken, useTheme } from 'tamagui'
import { getToken, getTokenValue, useTheme } from 'tamagui'
import AccountTab from './components/account-tab'
import Icon from '../Global/components/icon'
import PreferencesTab from './components/preferences-tab'
import PlaybackTab from './components/playback-tab'
import InfoTab from './components/info-tab'
import SettingsTabBar from './components/tab-bar'
import SettingsTabBar from './tab-bar'
import StorageTab from './components/usage-tab'
import { SafeAreaView } from 'react-native-safe-area-context'
const SettingsTabsNavigator = createMaterialTopTabNavigator()
@@ -15,111 +15,113 @@ export default function Settings(): React.JSX.Element {
const theme = useTheme()
return (
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
<SettingsTabsNavigator.Navigator
screenOptions={{
tabBarGap: getToken('$size.0'),
tabBarScrollEnabled: true,
tabBarItemStyle: {
width: getToken('$size.8'),
},
tabBarShowIcon: true,
tabBarActiveTintColor: theme.primary.val,
tabBarInactiveTintColor: theme.borderColor.val,
tabBarLabelStyle: {
fontFamily: 'Figtree-Bold',
},
<SettingsTabsNavigator.Navigator
screenOptions={{
tabBarShowIcon: true,
tabBarItemStyle: {
height: getToken('$12') + getToken('$6'),
},
tabBarActiveTintColor: theme.background.val,
tabBarInactiveTintColor: theme.background50.val,
tabBarStyle: {
backgroundColor: theme.primary.val,
},
tabBarLabelStyle: {
fontFamily: 'Figtree-Bold',
},
tabBarPressOpacity: 0.5,
lazy: true, // Enable lazy loading to prevent all tabs from mounting simultaneously
}}
tabBar={(props) => <SettingsTabBar {...props} />}
>
<SettingsTabsNavigator.Screen
name='Settings'
component={PreferencesTab}
options={{
title: 'App',
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name={`jellyfish${!focused ? '-outline' : ''}`}
color={focused ? '$background' : '$background50'}
small
/>
),
}}
tabBar={(props) => <SettingsTabBar {...props} />}
>
<SettingsTabsNavigator.Screen
name='Settings'
component={PreferencesTab}
options={{
title: 'App',
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name={`jellyfish${!focused ? '-outline' : ''}`}
color={focused ? '$primary' : '$borderColor'}
small
/>
),
}}
/>
/>
<SettingsTabsNavigator.Screen
name='Playback'
component={PlaybackTab}
options={{
title: 'Player',
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name='cassette'
color={focused ? '$primary' : '$borderColor'}
small
/>
),
}}
/>
<SettingsTabsNavigator.Screen
name='Playback'
component={PlaybackTab}
options={{
title: 'Player',
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name='cassette'
color={focused ? '$background' : '$background50'}
small
/>
),
}}
/>
<SettingsTabsNavigator.Screen
name='Usage'
component={StorageTab}
options={{
title: 'Usage',
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name='harddisk'
color={focused ? '$primary' : '$borderColor'}
small
/>
),
}}
/>
<SettingsTabsNavigator.Screen
name='Usage'
component={StorageTab}
options={{
title: 'Usage',
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name='harddisk'
color={focused ? '$background' : '$background50'}
small
/>
),
}}
/>
<SettingsTabsNavigator.Screen
name='User'
component={AccountTab}
options={{
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name='account-music'
color={focused ? '$primary' : '$borderColor'}
small
/>
),
}}
/>
<SettingsTabsNavigator.Screen
name='User'
component={AccountTab}
options={{
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name='account-music'
color={focused ? '$background' : '$background50'}
small
/>
),
}}
/>
<SettingsTabsNavigator.Screen
name='About'
component={InfoTab}
options={{
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
<Icon
name={`information${!focused ? '-outline' : ''}`}
color={focused ? '$background' : '$background50'}
small
/>
),
}}
/>
{/*
<SettingsTabsNavigator.Screen
name='About'
component={InfoTab}
name='Labs'
component={LabsTab}
options={{
tabBarIcon: ({ focused, color }: { focused: boolean; color: string }) => (
tabBarIcon: ({ focused, color }) => (
<Icon
name={`information${!focused ? '-outline' : ''}`}
name='flask'
color={focused ? '$primary' : '$borderColor'}
small
/>
),
}}
/>
{/*
<SettingsTabsNavigator.Screen
name='Labs'
component={LabsTab}
options={{
tabBarIcon: ({ focused, color }) => (
<Icon
name='flask'
color={focused ? '$primary' : '$borderColor'}
small
/>
),
}}
/>
) */}
</SettingsTabsNavigator.Navigator>
</SafeAreaView>
) */}
</SettingsTabsNavigator.Navigator>
)
}

View File

@@ -44,7 +44,9 @@ export default function PreferencesTab(): React.JSX.Element {
borderRadius={'$10'}
icon={<Icon name={icon} color={active ? '$background' : '$color'} small />}
>
<SizableText size={'$2'}>{label}</SizableText>
<SizableText color={active ? '$background' : '$color'} size={'$2'}>
{label}
</SizableText>
</Button>
)
@@ -57,7 +59,7 @@ export default function PreferencesTab(): React.JSX.Element {
case 'oled':
return 'Back in black'
default:
return undefined
return "I'm down with this system"
}
}, [themeSetting])

View File

@@ -1,7 +0,0 @@
import { MaterialTopTabBarProps, MaterialTopTabBar } from '@react-navigation/material-top-tabs'
export default function SettingsTabBar(props: MaterialTopTabBarProps): React.JSX.Element {
const { state, descriptors, navigation } = props
return <MaterialTopTabBar {...props} />
}

View File

@@ -0,0 +1,16 @@
import { MaterialTopTabBarProps, MaterialTopTabBar } from '@react-navigation/material-top-tabs'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { Square, YStack } from 'tamagui'
import StatusBar from '../Global/helpers/status-bar'
export default function SettingsTabBar(props: MaterialTopTabBarProps): React.JSX.Element {
const { top } = useSafeAreaInsets()
return (
<YStack>
<Square height={top} backgroundColor={'$primary'} />
<StatusBar invertColors />
<MaterialTopTabBar {...props} />
</YStack>
)
}

View File

@@ -1,6 +1,6 @@
import React, { RefObject, useMemo, useRef, useCallback, useEffect } from 'react'
import Track from '../Global/components/track'
import { getToken, Separator, XStack, YStack } from 'tamagui'
import { getToken, Separator, useTheme, XStack, YStack } from 'tamagui'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { Queue } from '../../player/types/queue-item'
import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list'
@@ -13,6 +13,7 @@ import { debounce, isString } from 'lodash'
import { RefreshControl } from 'react-native-gesture-handler'
import useItemContext from '../../hooks/use-item-context'
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header'
interface TracksProps {
tracksInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
@@ -29,6 +30,8 @@ export default function Tracks({
navigation,
queue,
}: TracksProps): React.JSX.Element {
const theme = useTheme()
const warmContext = useItemContext()
const sectionListRef = useRef<FlashListRef<string | number | BaseItemDto>>(null)
@@ -70,18 +73,7 @@ export default function Tracks({
const renderItem = useCallback(
({ item: track }: { index: number; item: string | number | BaseItemDto }) =>
typeof track === 'string' ? (
<XStack
padding={'$2'}
backgroundColor={'$background'}
borderRadius={'$5'}
borderWidth={'$1'}
borderColor={'$primary'}
marginRight={'$2'}
>
<Text bold color={'$primary'}>
{track.toUpperCase()}
</Text>
</XStack>
<FlashListStickyHeader text={track.toUpperCase()} />
) : typeof track === 'number' ? null : typeof track === 'object' ? (
<Track
navigation={navigation}
@@ -153,6 +145,7 @@ export default function Tracks({
<RefreshControl
refreshing={tracksInfiniteQuery.isFetching}
onRefresh={tracksInfiniteQuery.refetch}
tintColor={theme.primary.val}
/>
}
onEndReached={() => {

View File

@@ -0,0 +1,26 @@
import { useColorScheme } from 'react-native'
import { useThemeSetting } from '../stores/settings/app'
/**
* A hook that returns whether the user is
* running Jellify under a light mode configuration, be it
* configured at the app level or at the system level
*
* App level settings will _always_ override the system level
* settings
*/
export default function useIsLightMode() {
const [themeSetting] = useThemeSetting()
const systemSetting = useColorScheme()
switch (themeSetting) {
case 'light':
return true
case 'dark':
case 'oled':
return false
default:
return systemSetting === 'light'
}
}

View File

@@ -27,7 +27,7 @@ export default function Tabs({ route, navigation }: TabProps): React.JSX.Element
screenOptions={{
animation: 'shift',
tabBarActiveTintColor: theme.primary.val,
tabBarInactiveTintColor: theme.neutral.val,
tabBarInactiveTintColor: theme.borderColor.val,
lazy: true,
}}
tabBar={(props) => <TabBar {...props} />}

1742
yarn.lock

File diff suppressed because it is too large Load Diff