From 085269f51bdcc81c4927d32ca5f72ee9316e0452 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Sat, 6 Dec 2025 14:30:23 -0600 Subject: [PATCH] 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 --- src/api/queries/album/index.ts | 4 +- src/api/queries/artist/index.ts | 4 +- src/api/queries/track/index.ts | 4 +- src/components/Albums/component.tsx | 5 +- src/components/Artists/component.tsx | 4 +- .../Library/components/tracks-tab.tsx | 4 +- src/components/Library/tab-bar.tsx | 16 +- src/providers/Library/index.tsx | 60 ------- src/screens/Library/index.tsx | 151 +++++++++--------- src/stores/library.ts | 35 ++++ 10 files changed, 131 insertions(+), 156 deletions(-) delete mode 100644 src/providers/Library/index.tsx create mode 100644 src/stores/library.ts diff --git a/src/api/queries/album/index.ts b/src/api/queries/album/index.ts index 6bfde584..565ad6ad 100644 --- a/src/api/queries/album/index.ts +++ b/src/api/queries/album/index.ts @@ -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>, @@ -20,7 +20,7 @@ const useAlbums: () => [ const [user] = useJellifyUser() const [library] = useJellifyLibrary() - const { isFavorites } = useLibrarySortAndFilterContext() + const isFavorites = useLibraryStore((state) => state.isFavorites) const albumPageParams = useRef>(new Set()) diff --git a/src/api/queries/artist/index.ts b/src/api/queries/artist/index.ts index 569e9880..3a80380b 100644 --- a/src/api/queries/artist/index.ts +++ b/src/api/queries/artist/index.ts @@ -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>(new Set()) diff --git a/src/api/queries/track/index.ts b/src/api/queries/track/index.ts index da0ddefc..74962467 100644 --- a/src/api/queries/track/index.ts +++ b/src/api/queries/track/index.ts @@ -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) diff --git a/src/components/Albums/component.tsx b/src/components/Albums/component.tsx index 076b2614..e417d194 100644 --- a/src/components/Albums/component.tsx +++ b/src/components/Albums/component.tsx @@ -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>() @@ -38,7 +38,6 @@ export default function Albums({ const pendingLetterRef = useRef(null) - // Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations const stickyHeaderIndices = !showAlphabeticalSelector || !albumsInfiniteQuery.data ? [] diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx index 14d67e69..21f5364c 100644 --- a/src/components/Artists/component.tsx +++ b/src/components/Artists/component.tsx @@ -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>() diff --git a/src/components/Library/components/tracks-tab.tsx b/src/components/Library/components/tracks-tab.tsx index b36129ba..3fcee498 100644 --- a/src/components/Library/components/tracks-tab.tsx +++ b/src/components/Library/components/tracks-tab.tsx @@ -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>() diff --git a/src/components/Library/tab-bar.tsx b/src/components/Library/tab-bar.tsx index d074799f..cf9a91e3 100644 --- a/src/components/Library/tab-bar.tsx +++ b/src/components/Library/tab-bar.tsx @@ -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) { { 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'} > diff --git a/src/providers/Library/index.tsx b/src/providers/Library/index.tsx deleted file mode 100644 index 055530bc..00000000 --- a/src/providers/Library/index.tsx +++ /dev/null @@ -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(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({ - sortDescending: false, - setSortDescending: () => {}, - isFavorites: false, - setIsFavorites: () => {}, - isDownloaded: false, - setIsDownloaded: () => {}, -}) - -export const LibrarySortAndFilterProvider = ({ children }: { children: React.ReactNode }) => { - const context = LibrarySortAndFilterContextInitializer() - - return ( - - {children} - - ) -} - -export const useLibrarySortAndFilterContext = () => useContext(LibrarySortAndFilterContext) diff --git a/src/screens/Library/index.tsx b/src/screens/Library/index.tsx index d2fed71a..361be608 100644 --- a/src/screens/Library/index.tsx +++ b/src/screens/Library/index.tsx @@ -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 ( - - + + + + ({ + title: route.params.artist.Name ?? 'Unknown Artist', + headerTitleStyle: { + color: theme.background.val, + }, + })} + /> + + ({ + title: route.params.album.Name ?? 'Untitled Album', + headerTitleStyle: { + color: theme.background.val, + }, + })} + /> + + ({ + title: route.params.playlist.Name ?? 'Untitled Playlist', + headerTitleStyle: { + color: theme.background.val, + }, + })} + /> + + ({ + headerTitle: `${getItemName(route.params.item)} Mix`, + })} + /> + + - // 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 + - - ({ - title: route.params.artist.Name ?? 'Unknown Artist', - headerTitleStyle: { - color: theme.background.val, - }, - })} - /> - - ({ - title: route.params.album.Name ?? 'Untitled Album', - headerTitleStyle: { - color: theme.background.val, - }, - })} - /> - - ({ - title: route.params.playlist.Name ?? 'Untitled Playlist', - headerTitleStyle: { - color: theme.background.val, - }, - })} - /> - - ({ - headerTitle: `${getItemName(route.params.item)} Mix`, - })} - /> - - - - - - - - + + ) } diff --git a/src/stores/library.ts b/src/stores/library.ts new file mode 100644 index 00000000..2088b5cf --- /dev/null +++ b/src/stores/library.ts @@ -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()( + 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