mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-21 13:30:11 -06:00
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:
@@ -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": {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
38
src/components/Global/helpers/flashlist-sticky-header.tsx
Normal file
38
src/components/Global/helpers/flashlist-sticky-header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
21
src/components/Global/helpers/status-bar.tsx
Normal file
21
src/components/Global/helpers/status-bar.tsx
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
16
src/components/Settings/tab-bar.tsx
Normal file
16
src/components/Settings/tab-bar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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={() => {
|
||||
|
||||
26
src/hooks/use-is-light-mode.ts
Normal file
26
src/hooks/use-is-light-mode.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
@@ -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} />}
|
||||
|
||||
Reference in New Issue
Block a user