mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-06 11:00:09 -06:00
Fix Sort Controls in Library (#789)
Tears down the library sort and filter context provider in favor of an MMKV backed Zustand Store Adds some subtle animations for when users are pressing on the filters / controls in the library bar
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'
|
||||
@@ -11,6 +10,7 @@ import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||
import { fetchRecentlyAdded } from '../recents/utils'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
|
||||
const useAlbums: () => [
|
||||
RefObject<Set<string>>,
|
||||
@@ -20,7 +20,7 @@ const useAlbums: () => [
|
||||
const [user] = useJellifyUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const { isFavorites } = useLibrarySortAndFilterContext()
|
||||
const isFavorites = useLibraryStore((state) => state.isFavorites)
|
||||
|
||||
const albumPageParams = useRef<Set<string>>(new Set<string>())
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import { isUndefined } from 'lodash'
|
||||
import { fetchArtistAlbums, fetchArtistFeaturedOn, fetchArtists } from './utils/artist'
|
||||
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||
import { RefObject, useCallback, useRef } from 'react'
|
||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
||||
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
|
||||
export const useArtistAlbums = (artist: BaseItemDto) => {
|
||||
const api = useApi()
|
||||
@@ -44,7 +44,7 @@ export const useAlbumArtists: () => [
|
||||
const [user] = useJellifyUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const { isFavorites, sortDescending } = useLibrarySortAndFilterContext()
|
||||
const { isFavorites, sortDescending } = useLibraryStore()
|
||||
|
||||
const artistPageParams = useRef<Set<string>>(new Set<string>())
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'
|
||||
import { TracksQueryKey } from './keys'
|
||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
||||
import fetchTracks from './utils'
|
||||
import {
|
||||
BaseItemDto,
|
||||
@@ -16,6 +15,7 @@ import { queryClient } from '../../../constants/query-client'
|
||||
import UserDataQueryKey from '../user-data/keys'
|
||||
import { JellifyUser } from '@/src/types/JellifyUser'
|
||||
import { useApi, useJellifyUser, useJellifyLibrary } from '../../../stores'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
|
||||
const useTracks: (
|
||||
artistId?: string,
|
||||
@@ -35,7 +35,7 @@ const useTracks: (
|
||||
isFavorites: isLibraryFavorites,
|
||||
sortDescending: isLibrarySortDescending,
|
||||
isDownloaded,
|
||||
} = useLibrarySortAndFilterContext()
|
||||
} = useLibraryStore()
|
||||
|
||||
// Use provided values or fallback to library context
|
||||
// If artistId is present, we use isFavoritesParam if provided, otherwise false (default to showing all artist tracks)
|
||||
|
||||
@@ -12,8 +12,8 @@ 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'
|
||||
import { useLibrarySortAndFilterContext } from '../../providers/Library'
|
||||
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
||||
import useLibraryStore from '../../stores/library'
|
||||
|
||||
interface AlbumsProps {
|
||||
albumsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
@@ -30,7 +30,7 @@ export default function Albums({
|
||||
|
||||
const albums = albumsInfiniteQuery.data ?? []
|
||||
|
||||
const { isFavorites } = useLibrarySortAndFilterContext()
|
||||
const isFavorites = useLibraryStore((state) => state.isFavorites)
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function Albums({
|
||||
|
||||
const pendingLetterRef = useRef<string | null>(null)
|
||||
|
||||
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
|
||||
const stickyHeaderIndices =
|
||||
!showAlphabeticalSelector || !albumsInfiniteQuery.data
|
||||
? []
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Separator, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import { useLibrarySortAndFilterContext } from '../../providers/Library'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
||||
import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector'
|
||||
@@ -14,6 +13,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import LibraryStackParamList from '../../screens/Library/types'
|
||||
import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header'
|
||||
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
||||
import useLibraryStore from '../../stores/library'
|
||||
|
||||
export interface ArtistsProps {
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<
|
||||
@@ -38,7 +38,7 @@ export default function Artists({
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
const { isFavorites } = useLibrarySortAndFilterContext()
|
||||
const isFavorites = useLibraryStore((state) => state.isFavorites)
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
import Tracks from '../../Tracks/component'
|
||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import LibraryStackParamList from '@/src/screens/Library/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import useTracks from '../../../api/queries/track'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
|
||||
function TracksTab(): React.JSX.Element {
|
||||
const [trackPageParams, tracksInfiniteQuery] = useTracks()
|
||||
|
||||
const { isFavorites, isDownloaded } = useLibrarySortAndFilterContext()
|
||||
const { isFavorites, isDownloaded } = useLibraryStore()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { MaterialTopTabBar, MaterialTopTabBarProps } from '@react-navigation/material-top-tabs'
|
||||
import React from 'react'
|
||||
import { getTokenValue, Square, XStack, YStack } from 'tamagui'
|
||||
import { 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'
|
||||
import useLibraryStore from '../../stores/library'
|
||||
|
||||
function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
const { isFavorites, setIsFavorites, isDownloaded, setIsDownloaded } =
|
||||
useLibrarySortAndFilterContext()
|
||||
const { isFavorites, setIsFavorites, isDownloaded, setIsDownloaded } = useLibraryStore()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
@@ -39,6 +37,8 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
trigger('impactLight')
|
||||
props.navigation.navigate('AddPlaylist')
|
||||
}}
|
||||
pressStyle={{ opacity: 0.6 }}
|
||||
animation='quick'
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
@@ -50,8 +50,10 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
<XStack
|
||||
onPress={() => {
|
||||
trigger('impactLight')
|
||||
setIsFavorites(!isUndefined(isFavorites) ? undefined : true)
|
||||
setIsFavorites(!isFavorites)
|
||||
}}
|
||||
pressStyle={{ opacity: 0.6 }}
|
||||
animation='quick'
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
@@ -72,6 +74,8 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
trigger('impactLight')
|
||||
setIsDownloaded(!isDownloaded)
|
||||
}}
|
||||
pressStyle={{ opacity: 0.6 }}
|
||||
animation='quick'
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { storage } from '../../constants/storage'
|
||||
import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { createContext } from 'react'
|
||||
|
||||
interface LibrarySortAndFilterContext {
|
||||
sortDescending: boolean
|
||||
setSortDescending: (sortDescending: boolean) => void
|
||||
isFavorites: boolean | undefined
|
||||
setIsFavorites: (isFavorites: boolean | undefined) => void
|
||||
isDownloaded: boolean
|
||||
setIsDownloaded: (isDownloaded: boolean) => void
|
||||
}
|
||||
|
||||
const LibrarySortAndFilterContextInitializer = () => {
|
||||
const sortDescendingInit = storage.getBoolean(MMKVStorageKeys.LibrarySortDescending)
|
||||
const isFavoritesInit = storage.getBoolean(MMKVStorageKeys.LibraryIsFavorites)
|
||||
const isDownloadedInit = storage.getBoolean(MMKVStorageKeys.LibraryIsDownloaded)
|
||||
|
||||
const [sortDescending, setSortDescending] = useState(sortDescendingInit ?? false)
|
||||
const [isFavorites, setIsFavorites] = useState<boolean | undefined>(isFavoritesInit)
|
||||
const [isDownloaded, setIsDownloaded] = useState(isDownloadedInit ?? false)
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.LibrarySortDescending, sortDescending)
|
||||
storage.set(MMKVStorageKeys.LibraryIsDownloaded, isDownloaded)
|
||||
|
||||
if (isFavorites !== undefined) storage.set(MMKVStorageKeys.LibraryIsFavorites, isFavorites)
|
||||
else storage.delete(MMKVStorageKeys.LibraryIsFavorites)
|
||||
}, [sortDescending, isFavorites, isDownloaded])
|
||||
|
||||
return {
|
||||
sortDescending,
|
||||
setSortDescending,
|
||||
isFavorites,
|
||||
setIsFavorites,
|
||||
isDownloaded,
|
||||
setIsDownloaded,
|
||||
}
|
||||
}
|
||||
const LibrarySortAndFilterContext = createContext<LibrarySortAndFilterContext>({
|
||||
sortDescending: false,
|
||||
setSortDescending: () => {},
|
||||
isFavorites: false,
|
||||
setIsFavorites: () => {},
|
||||
isDownloaded: false,
|
||||
setIsDownloaded: () => {},
|
||||
})
|
||||
|
||||
export const LibrarySortAndFilterProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const context = LibrarySortAndFilterContextInitializer()
|
||||
|
||||
return (
|
||||
<LibrarySortAndFilterContext.Provider value={context}>
|
||||
{children}
|
||||
</LibrarySortAndFilterContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLibrarySortAndFilterContext = () => useContext(LibrarySortAndFilterContext)
|
||||
@@ -9,7 +9,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import AlbumScreen from '../Album'
|
||||
import LibraryStackParamList from './types'
|
||||
import { LibraryTabProps } from '../Tabs/types'
|
||||
import { LibrarySortAndFilterProvider } from '../../providers/Library'
|
||||
import InstantMix from '../../components/InstantMix/component'
|
||||
import { getItemName } from '../../utils/text'
|
||||
|
||||
@@ -19,87 +18,85 @@ export default function LibraryScreen({ route, navigation }: LibraryTabProps): R
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<LibrarySortAndFilterProvider>
|
||||
<LibraryStack.Navigator initialRouteName='LibraryScreen'>
|
||||
<LibraryStack.Navigator initialRouteName='LibraryScreen'>
|
||||
<LibraryStack.Screen
|
||||
name='LibraryScreen'
|
||||
component={Library}
|
||||
options={{
|
||||
title: 'Library',
|
||||
|
||||
// I honestly don't think we need a header for this screen, given that there are
|
||||
// tabs on the top of the screen for navigating the library, but if we want one,
|
||||
// we can use the title above
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='Artist'
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||
headerTitleStyle: {
|
||||
color: theme.background.val,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='Album'
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.album.Name ?? 'Untitled Album',
|
||||
headerTitleStyle: {
|
||||
color: theme.background.val,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='Playlist'
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.playlist.Name ?? 'Untitled Playlist',
|
||||
headerTitleStyle: {
|
||||
color: theme.background.val,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='InstantMix'
|
||||
component={InstantMix}
|
||||
options={({ route }) => ({
|
||||
headerTitle: `${getItemName(route.params.item)} Mix`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Group
|
||||
screenOptions={{
|
||||
presentation: 'formSheet',
|
||||
sheetAllowedDetents: 'fitToContents',
|
||||
}}
|
||||
>
|
||||
<LibraryStack.Screen
|
||||
name='LibraryScreen'
|
||||
component={Library}
|
||||
name='AddPlaylist'
|
||||
component={AddPlaylist}
|
||||
options={{
|
||||
title: 'Library',
|
||||
title: 'Add Playlist',
|
||||
}}
|
||||
/>
|
||||
|
||||
// I honestly don't think we need a header for this screen, given that there are
|
||||
// tabs on the top of the screen for navigating the library, but if we want one,
|
||||
// we can use the title above
|
||||
<LibraryStack.Screen
|
||||
name='DeletePlaylist'
|
||||
component={DeletePlaylist}
|
||||
options={{
|
||||
title: 'Delete Playlist',
|
||||
headerShown: false,
|
||||
sheetGrabberVisible: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='Artist'
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||
headerTitleStyle: {
|
||||
color: theme.background.val,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='Album'
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.album.Name ?? 'Untitled Album',
|
||||
headerTitleStyle: {
|
||||
color: theme.background.val,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='Playlist'
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.playlist.Name ?? 'Untitled Playlist',
|
||||
headerTitleStyle: {
|
||||
color: theme.background.val,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='InstantMix'
|
||||
component={InstantMix}
|
||||
options={({ route }) => ({
|
||||
headerTitle: `${getItemName(route.params.item)} Mix`,
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Group
|
||||
screenOptions={{
|
||||
presentation: 'formSheet',
|
||||
sheetAllowedDetents: 'fitToContents',
|
||||
}}
|
||||
>
|
||||
<LibraryStack.Screen
|
||||
name='AddPlaylist'
|
||||
component={AddPlaylist}
|
||||
options={{
|
||||
title: 'Add Playlist',
|
||||
}}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name='DeletePlaylist'
|
||||
component={DeletePlaylist}
|
||||
options={{
|
||||
title: 'Delete Playlist',
|
||||
headerShown: false,
|
||||
sheetGrabberVisible: true,
|
||||
}}
|
||||
/>
|
||||
</LibraryStack.Group>
|
||||
</LibraryStack.Navigator>
|
||||
</LibrarySortAndFilterProvider>
|
||||
</LibraryStack.Group>
|
||||
</LibraryStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
35
src/stores/library.ts
Normal file
35
src/stores/library.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware'
|
||||
import { mmkvStateStorage } from '../constants/storage'
|
||||
import { create } from 'zustand'
|
||||
|
||||
type LibraryStore = {
|
||||
sortDescending: boolean
|
||||
setSortDescending: (sortDescending: boolean) => void
|
||||
isFavorites: boolean
|
||||
setIsFavorites: (isFavorites: boolean) => void
|
||||
isDownloaded: boolean
|
||||
setIsDownloaded: (isDownloaded: boolean) => void
|
||||
}
|
||||
|
||||
const useLibraryStore = create<LibraryStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
sortDescending: false,
|
||||
setSortDescending: (sortDescending: boolean) => set({ sortDescending }),
|
||||
|
||||
isFavorites: false,
|
||||
setIsFavorites: (isFavorites: boolean) => set({ isFavorites }),
|
||||
|
||||
isDownloaded: false,
|
||||
setIsDownloaded: (isDownloaded: boolean) => set({ isDownloaded }),
|
||||
}),
|
||||
{
|
||||
name: 'library-store',
|
||||
storage: createJSONStorage(() => mmkvStateStorage),
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
export default useLibraryStore
|
||||
Reference in New Issue
Block a user