From 18d8d0bb59f08fecec5d36f954b1d372661df6f8 Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Tue, 4 Nov 2025 06:39:32 -0600 Subject: [PATCH] Maintenance/migrate jellify context to zustand (#640) Remove `JellifyContext` provider in favor of a persisted Zustand store This should address issues where the user would open the app and be randomly signed out and have to sign back in --- ios/CarScene.swift | 3 +- ios/Jellify.xcodeproj/project.pbxproj | 20 +- jest/contextual/JellifyProvider.test.tsx | 62 ------ jest/contextual/PlayerProvider.test.tsx | 5 +- maestro/tests/6-settings.yaml | 4 +- src/api/mutations/authentication/index.ts | 13 +- src/api/mutations/download/index.ts | 4 +- src/api/mutations/favorite/index.ts | 8 +- src/api/mutations/playback/index.ts | 8 +- src/api/mutations/public-system-info/index.ts | 4 +- src/api/queries/album/index.ts | 19 +- src/api/queries/artist/index.ts | 14 +- src/api/queries/frequents/index.ts | 10 +- src/api/queries/lyrics/index.ts | 4 +- src/api/queries/media/index.ts | 15 +- src/api/queries/patrons/index.ts | 4 +- src/api/queries/playlist/index.ts | 8 +- src/api/queries/playlist/utils/index.ts | 6 +- src/api/queries/recents/index.ts | 10 +- src/api/queries/track/index.ts | 6 +- src/api/queries/user-data/index.ts | 5 +- src/components/AddToPlaylist/index.tsx | 5 +- src/components/Album/index.tsx | 6 +- src/components/Artist/header.tsx | 5 +- src/components/Context/index.tsx | 10 +- .../Discover/helpers/public-playlists.tsx | 5 +- src/components/Global/components/image.tsx | 8 +- .../Global/components/instant-mix-button.tsx | 6 +- src/components/Global/components/item-row.tsx | 8 +- .../Global/components/library-selector.tsx | 6 +- src/components/Global/components/track.tsx | 4 +- .../Home/helpers/frequent-tracks.tsx | 5 +- .../Home/helpers/recently-played.tsx | 5 +- .../Player/components/song-info.tsx | 4 +- src/components/Playlist/components/header.tsx | 9 +- src/components/Search/index.tsx | 7 +- .../Settings/components/account-tab.tsx | 6 +- src/components/jellify.tsx | 8 +- src/hooks/use-item-context.ts | 5 +- src/providers/Album/index.tsx | 4 +- src/providers/Artist/index.tsx | 6 +- src/providers/CarPlay/index.tsx | 9 +- src/providers/Discover/index.tsx | 7 +- src/providers/Player/index.tsx | 4 +- src/providers/Playlist/index.tsx | 4 +- src/providers/index.tsx | 199 ------------------ src/screens/Library/add-playlist.tsx | 6 +- src/screens/Library/delete-playlist.tsx | 6 +- src/screens/Login/index.tsx | 5 +- src/screens/Login/server-address.tsx | 4 +- src/screens/Login/server-authentication.tsx | 4 +- src/screens/Login/server-library.tsx | 4 +- src/screens/Settings/library-selection.tsx | 93 ++++---- src/screens/Settings/sign-out-modal.tsx | 4 +- src/screens/index.tsx | 12 +- src/stores/index.ts | 104 +++++++++ 56 files changed, 344 insertions(+), 475 deletions(-) delete mode 100644 jest/contextual/JellifyProvider.test.tsx delete mode 100644 src/providers/index.tsx create mode 100644 src/stores/index.ts diff --git a/ios/CarScene.swift b/ios/CarScene.swift index bb99cf4f..ab237db4 100644 --- a/ios/CarScene.swift +++ b/ios/CarScene.swift @@ -5,13 +5,12 @@ import CarPlay class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) { - RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow); + RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow) } func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) { RNCarPlay.disconnect() - } } diff --git a/ios/Jellify.xcodeproj/project.pbxproj b/ios/Jellify.xcodeproj/project.pbxproj index ef50cf6d..d3b7a22e 100644 --- a/ios/Jellify.xcodeproj/project.pbxproj +++ b/ios/Jellify.xcodeproj/project.pbxproj @@ -93,8 +93,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ CFE47DDB2EA56B0200EB6067 /* icons */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = icons; sourceTree = ""; }; @@ -397,10 +395,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n"; @@ -414,10 +416,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n"; @@ -697,10 +703,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -787,10 +790,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/jest/contextual/JellifyProvider.test.tsx b/jest/contextual/JellifyProvider.test.tsx deleted file mode 100644 index 8865b52d..00000000 --- a/jest/contextual/JellifyProvider.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react-native' -import { JellifyProvider, useJellifyContext } from '../../src/providers' -import { Text, View } from 'react-native' -import { MMKVStorageKeys } from '../../src/enums/mmkv-storage-keys' -import { storage } from '../../src/constants/storage' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' - -const queryClient = new QueryClient() - -const JellifyConsumer = () => { - const { server, user, library } = useJellifyContext() - - return ( - - {server?.url} - {user?.name} - {library?.musicLibraryName} - - ) -} - -test(`${JellifyProvider.name} renders correctly`, async () => { - storage.set( - MMKVStorageKeys.Server, - JSON.stringify({ - url: 'http://localhost:8096', - }), - ) - - storage.set( - MMKVStorageKeys.User, - JSON.stringify({ - name: 'Violet Caulfield', - }), - ) - - storage.set( - MMKVStorageKeys.Library, - JSON.stringify({ - musicLibraryName: 'Music Library', - }), - ) - - render( - - - - - , - , - ) - - const apiBasePath = screen.getByTestId('api-base-path') - const userName = screen.getByTestId('user-name') - const libraryName = screen.getByTestId('library-name') - - await waitFor(() => { - expect(apiBasePath.props.children).toBe('http://localhost:8096') - expect(userName.props.children).toBe('Violet Caulfield') - expect(libraryName.props.children).toBe('Music Library') - }) -}) diff --git a/jest/contextual/PlayerProvider.test.tsx b/jest/contextual/PlayerProvider.test.tsx index cf5954e4..22943684 100644 --- a/jest/contextual/PlayerProvider.test.tsx +++ b/jest/contextual/PlayerProvider.test.tsx @@ -4,16 +4,13 @@ import { render } from '@testing-library/react-native' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { PlayerProvider } from '../../src/providers/Player' -import { JellifyProvider } from '../../src/providers' const queryClient = new QueryClient() test(`${PlayerProvider.name} renders correctly`, () => { render( - - - + , ) }) diff --git a/maestro/tests/6-settings.yaml b/maestro/tests/6-settings.yaml index 072623fc..65a04b14 100644 --- a/maestro/tests/6-settings.yaml +++ b/maestro/tests/6-settings.yaml @@ -14,9 +14,9 @@ appId: com.cosmonautical.jellify # Test App (Preferences) Tab - should already be selected - assertVisible: - text: "Send Metrics and Crash Reports" + text: "Send Analytics" - assertVisible: - text: "Send anonymous usage and crash data" + text: "Send usage and crash data" - assertVisible: text: "Reduce Haptics" - assertVisible: diff --git a/src/api/mutations/authentication/index.ts b/src/api/mutations/authentication/index.ts index 2c871840..08867f87 100644 --- a/src/api/mutations/authentication/index.ts +++ b/src/api/mutations/authentication/index.ts @@ -2,9 +2,10 @@ import { AxiosResponse } from 'axios' import { JellyfinCredentials } from '../../types/jellyfin-credentials' import { AuthenticationResult } from '@jellyfin/sdk/lib/generated-client' import { useMutation } from '@tanstack/react-query' -import { useJellifyContext } from '../../../providers' import { JellifyUser } from '../../../types/JellifyUser' import { isUndefined } from 'lodash' +import { getUserApi } from '@jellyfin/sdk/lib/utils/api' +import { useApi, useJellifyUser } from '../../../stores' interface AuthenticateUserByNameMutation { onSuccess?: () => void @@ -12,11 +13,17 @@ interface AuthenticateUserByNameMutation { } const useAuthenticateUserByName = ({ onSuccess, onError }: AuthenticateUserByNameMutation) => { - const { api, setUser } = useJellifyContext() + const api = useApi() + const [user, setUser] = useJellifyUser() return useMutation({ mutationFn: async (credentials: JellyfinCredentials) => { - return await api!.authenticateUserByName(credentials.username, credentials.password) + return await getUserApi(api!).authenticateUserByName({ + authenticateUserByName: { + Username: credentials.username, + Pw: credentials.password, + }, + }) }, onSuccess: async (authResult: AxiosResponse) => { console.log(`Received auth response from server`) diff --git a/src/api/mutations/download/index.ts b/src/api/mutations/download/index.ts index 2e454ebf..d6c48290 100644 --- a/src/api/mutations/download/index.ts +++ b/src/api/mutations/download/index.ts @@ -1,5 +1,4 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' -import { useJellifyContext } from '../../../providers' import { useDownloadingDeviceProfile } from '../../../stores/device-profile' import { UseMutateFunction, useMutation } from '@tanstack/react-query' import { mapDtoToTrack } from '../../../utils/mappings' @@ -7,12 +6,13 @@ import { deleteAudio, saveAudio } from './offlineModeUtils' import { useState } from 'react' import { JellifyDownloadProgress } from '../../../types/JellifyDownload' import { useAllDownloadedTracks } from '../../queries/download' +import { useApi } from '../../../stores' export const useDownloadAudioItem: () => [ JellifyDownloadProgress, UseMutateFunction, ] = () => { - const { api } = useJellifyContext() + const api = useApi() const { data: downloadedTracks, refetch } = useAllDownloadedTracks() diff --git a/src/api/mutations/favorite/index.ts b/src/api/mutations/favorite/index.ts index c09ea7fb..1c572e6a 100644 --- a/src/api/mutations/favorite/index.ts +++ b/src/api/mutations/favorite/index.ts @@ -1,12 +1,12 @@ import { queryClient } from '../../../constants/query-client' import useHapticFeedback from '../../../hooks/use-haptic-feedback' -import { useJellifyContext } from '../../../providers' import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client' import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api' import { useMutation } from '@tanstack/react-query' import { isUndefined } from 'lodash' import Toast from 'react-native-toast-message' import UserDataQueryKey from '../../queries/user-data/keys' +import { useApi, useJellifyUser } from '../../../../src/stores' interface SetFavoriteMutation { item: BaseItemDto @@ -14,7 +14,8 @@ interface SetFavoriteMutation { } export const useAddFavorite = () => { - const { api, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() const trigger = useHapticFeedback() @@ -59,7 +60,8 @@ export const useAddFavorite = () => { } export const useRemoveFavorite = () => { - const { api, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() const trigger = useHapticFeedback() diff --git a/src/api/mutations/playback/index.ts b/src/api/mutations/playback/index.ts index 4f331255..f0deaa27 100644 --- a/src/api/mutations/playback/index.ts +++ b/src/api/mutations/playback/index.ts @@ -1,4 +1,3 @@ -import { useJellifyContext } from '../../../providers' import JellifyTrack from '../../../types/JellifyTrack' import { useMutation } from '@tanstack/react-query' import reportPlaybackCompleted from './functions/playback-completed' @@ -6,13 +5,14 @@ import reportPlaybackStopped from './functions/playback-stopped' import isPlaybackFinished from './utils' import reportPlaybackProgress from './functions/playback-progress' import reportPlaybackStarted from './functions/playback-started' +import { useApi } from '../../../stores' interface PlaybackStartedMutation { track: JellifyTrack } export const useReportPlaybackStarted = () => { - const { api } = useJellifyContext() + const api = useApi() return useMutation({ onMutate: () => {}, @@ -29,7 +29,7 @@ interface PlaybackStoppedMutation { } export const useReportPlaybackStopped = () => { - const { api } = useJellifyContext() + const api = useApi() return useMutation({ onMutate: ({ lastPosition, duration }) => @@ -59,7 +59,7 @@ interface PlaybackProgressMutation { } export const useReportPlaybackProgress = () => { - const { api } = useJellifyContext() + const api = useApi() return useMutation({ onMutate: ({ position }) => console.debug(`Reporting progress at ${position}`), diff --git a/src/api/mutations/public-system-info/index.ts b/src/api/mutations/public-system-info/index.ts index b12eda78..badc07ab 100644 --- a/src/api/mutations/public-system-info/index.ts +++ b/src/api/mutations/public-system-info/index.ts @@ -3,7 +3,7 @@ import { connectToServer } from './utils' import { JellifyServer } from '@/src/types/JellifyServer' import serverAddressContainsProtocol from './utils/parsing' import HTTPS, { HTTP } from '../../../constants/protocols' -import { useJellifyContext } from '../../../providers' +import useJellifyStore from '../../../stores' interface PublicSystemInfoMutation { serverAddress: string @@ -16,7 +16,7 @@ interface PublicSystemInfoHook { } const usePublicSystemInfo = ({ onSuccess, onError }: PublicSystemInfoHook) => { - const { setServer } = useJellifyContext() + const setServer = useJellifyStore((state) => state.setServer) return useMutation({ mutationFn: ({ serverAddress, useHttps }: PublicSystemInfoMutation) => diff --git a/src/api/queries/album/index.ts b/src/api/queries/album/index.ts index d1b79a6b..2ce0261c 100644 --- a/src/api/queries/album/index.ts +++ b/src/api/queries/album/index.ts @@ -1,6 +1,5 @@ import { useLibrarySortAndFilterContext } from '../../../providers/Library' import { QueryKeys } from '../../../enums/query-keys' -import { useJellifyContext } from '../../../providers' import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query' import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by' import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order' @@ -11,14 +10,17 @@ import flattenInfiniteQueryPages from '../../../utils/query-selectors' import { ApiLimits } from '../query.config' import { fetchRecentlyAdded } from '../recents/utils' import { queryClient } from '../../../constants/query-client' +import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores' const useAlbums: () => [ RefObject>, UseInfiniteQueryResult<(string | number | BaseItemDto)[]>, ] = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() - const { isFavorites, sortDescending } = useLibrarySortAndFilterContext() + const { isFavorites } = useLibrarySortAndFilterContext() const albumPageParams = useRef>(new Set()) @@ -43,10 +45,10 @@ const useAlbums: () => [ ), initialPageParam: 0, select: selectAlbums, - getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => { + getNextPageParam: (lastPage, allPages, lastPageParam) => { return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined }, - getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => { + getPreviousPageParam: (firstPage, allPages, firstPageParam) => { return firstPageParam === 0 ? null : firstPageParam - 1 }, }) @@ -57,20 +59,21 @@ const useAlbums: () => [ export default useAlbums export const useRecentlyAddedAlbums = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [library] = useJellifyLibrary() return useInfiniteQuery({ queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId], queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam), select: (data) => data.pages.flatMap((page) => page), - getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => + getNextPageParam: (lastPage, allPages, lastPageParam) => lastPage.length > 0 ? lastPageParam + 1 : undefined, initialPageParam: 0, }) } export const useRefetchRecentlyAdded: () => () => void = () => { - const { library } = useJellifyContext() + const [library] = useJellifyLibrary() return () => queryClient.invalidateQueries({ diff --git a/src/api/queries/artist/index.ts b/src/api/queries/artist/index.ts index 1f3a2573..34d11160 100644 --- a/src/api/queries/artist/index.ts +++ b/src/api/queries/artist/index.ts @@ -6,16 +6,17 @@ import { UseInfiniteQueryResult, useQuery, } from '@tanstack/react-query' -import { isString, isUndefined } from 'lodash' +import { isUndefined } from 'lodash' import { fetchArtistAlbums, fetchArtistFeaturedOn, fetchArtists } from './utils/artist' -import { useJellifyContext } from '../../../providers' import { ApiLimits } from '../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' export const useArtistAlbums = (artist: BaseItemDto) => { - const { api, library } = useJellifyContext() + const api = useApi() + const [library] = useJellifyLibrary() return useQuery({ queryKey: [QueryKeys.ArtistAlbums, library?.musicLibraryId, artist.Id], @@ -25,7 +26,8 @@ export const useArtistAlbums = (artist: BaseItemDto) => { } export const useArtistFeaturedOn = (artist: BaseItemDto) => { - const { api, library } = useJellifyContext() + const api = useApi() + const [library] = useJellifyLibrary() return useQuery({ queryKey: [QueryKeys.ArtistFeaturedOn, library?.musicLibraryId, artist.Id], @@ -38,7 +40,9 @@ export const useAlbumArtists: () => [ RefObject>, UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>, ] = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const { isFavorites, sortDescending } = useLibrarySortAndFilterContext() diff --git a/src/api/queries/frequents/index.ts b/src/api/queries/frequents/index.ts index 0ccd9de5..f197a5c5 100644 --- a/src/api/queries/frequents/index.ts +++ b/src/api/queries/frequents/index.ts @@ -1,9 +1,9 @@ import { useInfiniteQuery } from '@tanstack/react-query' import { FrequentlyPlayedArtistsQueryKey, FrequentlyPlayedTracksQueryKey } from './keys' -import { useJellifyContext } from '../../../providers' import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from './utils/frequents' import { ApiLimits } from '../query.config' import { isUndefined } from 'lodash' +import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores' const FREQUENTS_QUERY_CONFIG = { refetchOnMount: false, @@ -11,7 +11,9 @@ const FREQUENTS_QUERY_CONFIG = { } as const export const useFrequentlyPlayedTracks = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() return useInfiniteQuery({ queryKey: FrequentlyPlayedTracksQueryKey(user, library), @@ -27,7 +29,9 @@ export const useFrequentlyPlayedTracks = () => { } export const useFrequentlyPlayedArtists = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const { data: frequentlyPlayedTracks } = useFrequentlyPlayedTracks() diff --git a/src/api/queries/lyrics/index.ts b/src/api/queries/lyrics/index.ts index 6b6a03d9..8b7fc600 100644 --- a/src/api/queries/lyrics/index.ts +++ b/src/api/queries/lyrics/index.ts @@ -2,7 +2,7 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query' import LyricsQueryKey from './keys' import { isUndefined } from 'lodash' import { fetchRawLyrics } from './utils' -import { useJellifyContext } from '../../../providers' +import { useApi } from '../../../stores' import { useCurrentTrack } from '../../../stores/player/queue' /** @@ -11,7 +11,7 @@ import { useCurrentTrack } from '../../../stores/player/queue' * @returns a {@link UseQueryResult} for the */ const useRawLyrics = () => { - const { api } = useJellifyContext() + const api = useApi() const nowPlaying = useCurrentTrack() return useQuery({ diff --git a/src/api/queries/media/index.ts b/src/api/queries/media/index.ts index b2c1f118..dde761c5 100644 --- a/src/api/queries/media/index.ts +++ b/src/api/queries/media/index.ts @@ -1,13 +1,12 @@ import { Api } from '@jellyfin/sdk' -import { useJellifyContext } from '../../../../src/providers' import { useQuery } from '@tanstack/react-query' -import { JellifyUser } from '@/src/types/JellifyUser' import useStreamingDeviceProfile, { useDownloadingDeviceProfile, } from '../../../stores/device-profile' import { fetchMediaInfo } from './utils' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' import MediaInfoQueryKey from './keys' +import { useApi } from '../../../stores' /** * A React hook that will retrieve the latest media info @@ -16,15 +15,15 @@ import MediaInfoQueryKey from './keys' * Depends on the {@link useStreamingDeviceProfile} hook for retrieving * the currently configured device profile * - * Depends on the {@link useJellifyContext} hook for retrieving - * the currently configured {@link Api} and {@link JellifyUser} + * Depends on the {@link useApi} hook for retrieving + * the currently configured {@link Api} * instance * * @param itemId The Id of the {@link BaseItemDto} * @returns */ const useStreamedMediaInfo = (itemId: string | null | undefined) => { - const { api } = useJellifyContext() + const api = useApi() const deviceProfile = useStreamingDeviceProfile() @@ -45,15 +44,15 @@ export default useStreamedMediaInfo * Depends on the {@link useDownloadingDeviceProfile} hook for retrieving * the currently configured device profile * - * Depends on the {@link useJellifyContext} hook for retrieving - * the currently configured {@link Api} and {@link JellifyUser} + * Depends on the {@link useApi} hook for retrieving + * the currently configured {@link Api} * instance * * @param itemId The Id of the {@link BaseItemDto} * @returns */ export const useDownloadedMediaInfo = (itemId: string | null | undefined) => { - const { api } = useJellifyContext() + const api = useApi() const deviceProfile = useDownloadingDeviceProfile() diff --git a/src/api/queries/patrons/index.ts b/src/api/queries/patrons/index.ts index 3cca14a8..a01b321e 100644 --- a/src/api/queries/patrons/index.ts +++ b/src/api/queries/patrons/index.ts @@ -1,11 +1,11 @@ import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../../enums/query-keys' -import { useJellifyContext } from '../../../providers' import fetchPatrons from './utils' import { ONE_DAY } from '../../../constants/query-client' +import { useApi } from '../../../stores' const usePatronsQuery = () => { - const { api } = useJellifyContext() + const api = useApi() return useQuery({ queryKey: [QueryKeys.Patrons], diff --git a/src/api/queries/playlist/index.ts b/src/api/queries/playlist/index.ts index 92885ca3..dac74a9a 100644 --- a/src/api/queries/playlist/index.ts +++ b/src/api/queries/playlist/index.ts @@ -1,14 +1,16 @@ -import { useJellifyContext } from '../../../providers' import { UserPlaylistsQueryKey } from './keys' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { fetchUserPlaylists, fetchPublicPlaylists } from './utils' import { ApiLimits } from '../query.config' +import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client' import { QueryKeys } from '../../../enums/query-keys' import { getItemsApi } from '@jellyfin/sdk/lib/utils/api' export const useUserPlaylists = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() return useInfiniteQuery({ queryKey: UserPlaylistsQueryKey(library), @@ -22,7 +24,7 @@ export const useUserPlaylists = () => { } export const usePlaylistTracks = (playlist: BaseItemDto) => { - const { api } = useJellifyContext() + const api = useApi() return useQuery({ queryKey: [QueryKeys.ItemTracks, playlist.Id!], diff --git a/src/api/queries/playlist/utils/index.ts b/src/api/queries/playlist/utils/index.ts index ec60e868..678b04ed 100644 --- a/src/api/queries/playlist/utils/index.ts +++ b/src/api/queries/playlist/utils/index.ts @@ -18,9 +18,9 @@ import QueryConfig from '../../query.config' * config directory of Jellyfin, as to avoid displaying .m3u files from * the library * - * @param api The {@link Api} instance from the {@link useJellifyContext} hook - * @param user The {@link JellifyUser} instance from the {@link useJellifyContext} hook - * @param library The {@link JellifyLibrary} instance from the {@link useJellifyContext} hook + * @param api The {@link Api} instance from the {@link useApi} hook + * @param user The {@link JellifyUser} instance from the {@link useJellifyUser} hook + * @param library The {@link JellifyLibrary} instance from the {@link useJellifyLibrary} hook * @param sortBy An array of {@link ItemSortBy} values to sort the response by * @returns */ diff --git a/src/api/queries/recents/index.ts b/src/api/queries/recents/index.ts index 594a4a87..0c268661 100644 --- a/src/api/queries/recents/index.ts +++ b/src/api/queries/recents/index.ts @@ -1,9 +1,9 @@ -import { useJellifyContext } from '../../../providers' import { RecentlyPlayedArtistsQueryKey, RecentlyPlayedTracksQueryKey } from './keys' import { useInfiniteQuery } from '@tanstack/react-query' import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from './utils' import { ApiLimits } from '../query.config' import { isUndefined } from 'lodash' +import { useApi, useJellifyUser, useJellifyLibrary } from '../../../stores' const RECENTS_QUERY_CONFIG = { maxPages: 2, @@ -12,7 +12,9 @@ const RECENTS_QUERY_CONFIG = { } as const export const useRecentlyPlayedTracks = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() return useInfiniteQuery({ queryKey: RecentlyPlayedTracksQueryKey(user, library), @@ -28,7 +30,9 @@ export const useRecentlyPlayedTracks = () => { } export const useRecentArtists = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const { data: recentlyPlayedTracks } = useRecentlyPlayedTracks() diff --git a/src/api/queries/track/index.ts b/src/api/queries/track/index.ts index 639967c8..d11e8505 100644 --- a/src/api/queries/track/index.ts +++ b/src/api/queries/track/index.ts @@ -1,7 +1,6 @@ import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query' import { TracksQueryKey } from './keys' import { useLibrarySortAndFilterContext } from '../../../providers/Library' -import { useJellifyContext } from '../../../providers' import fetchTracks from './utils' import { BaseItemDto, @@ -16,12 +15,15 @@ import { useAllDownloadedTracks } from '../download' import { queryClient } from '../../../constants/query-client' import UserDataQueryKey from '../user-data/keys' import { JellifyUser } from '@/src/types/JellifyUser' +import { useApi, useJellifyUser, useJellifyLibrary } from '../../../stores' const useTracks: () => [ RefObject>, UseInfiniteQueryResult<(string | number | BaseItemDto)[]>, ] = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const { isFavorites, sortDescending, isDownloaded } = useLibrarySortAndFilterContext() const { data: downloadedTracks } = useAllDownloadedTracks() diff --git a/src/api/queries/user-data/index.ts b/src/api/queries/user-data/index.ts index c5046af9..ceabace6 100644 --- a/src/api/queries/user-data/index.ts +++ b/src/api/queries/user-data/index.ts @@ -1,11 +1,12 @@ -import { useJellifyContext } from '../../../providers' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto' import { useQuery } from '@tanstack/react-query' import fetchUserData from './utils' import UserDataQueryKey from './keys' +import { useApi, useJellifyUser } from '../../../stores' export const useIsFavorite = (item: BaseItemDto) => { - const { api, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() return useQuery({ queryKey: UserDataQueryKey(user!, item), diff --git a/src/components/AddToPlaylist/index.tsx b/src/components/AddToPlaylist/index.tsx index 7458713c..1fb82da7 100644 --- a/src/components/AddToPlaylist/index.tsx +++ b/src/components/AddToPlaylist/index.tsx @@ -1,5 +1,4 @@ import { useMutation } from '@tanstack/react-query' -import { useJellifyContext } from '../../providers' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { addManyToPlaylist, addToPlaylist } from '../../api/mutations/playlists' import { useState } from 'react' @@ -15,6 +14,7 @@ import { getItemName } from '../../utils/text' import useHapticFeedback from '../../hooks/use-haptic-feedback' 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' export default function AddToPlaylist({ @@ -80,7 +80,8 @@ function AddToPlaylistRow({ playlist: BaseItemDto tracks: BaseItemDto[] }): React.JSX.Element { - const { api, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() const trigger = useHapticFeedback() diff --git a/src/components/Album/index.tsx b/src/components/Album/index.tsx index 105cde90..069af639 100644 --- a/src/components/Album/index.tsx +++ b/src/components/Album/index.tsx @@ -10,7 +10,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import InstantMixButton from '../Global/components/instant-mix-button' import ItemImage from '../Global/components/image' import React, { useCallback, useMemo } from 'react' -import { useJellifyContext } from '../../providers' import { useSafeAreaFrame } from 'react-native-safe-area-context' import Icon from '../Global/components/icon' import { mapDtoToTrack } from '../../utils/mappings' @@ -25,6 +24,7 @@ import LibraryStackParamList from '../../screens/Library/types' import DiscoverStackParamList from '../../screens/Discover/types' import { BaseStackParamList } from '../../screens/types' import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../../stores/device-profile' +import { useApi } from '../../stores' /** * The screen for an Album's track list @@ -39,7 +39,7 @@ export function Album(): React.JSX.Element { const { album, discs, isPending } = useAlbumContext() - const { api } = useJellifyContext() + const api = useApi() const { addToDownloadQueue, pendingDownloads } = useNetworkContext() const downloadingDeviceProfile = useDownloadingDeviceProfile() @@ -129,7 +129,7 @@ export function Album(): React.JSX.Element { * @returns A React component */ function AlbumTrackListHeader(): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const { width } = useSafeAreaFrame() diff --git a/src/components/Artist/header.tsx b/src/components/Artist/header.tsx index 81d37b0e..41ce7a56 100644 --- a/src/components/Artist/header.tsx +++ b/src/components/Artist/header.tsx @@ -1,7 +1,6 @@ import { ImageType } from '@jellyfin/sdk/lib/generated-client' import LinearGradient from 'react-native-linear-gradient' import { getTokenValue, useTheme, XStack, YStack, ZStack } from 'tamagui' -import Icon from '../Global/components/icon' import ItemImage from '../Global/components/image' import { useSafeAreaFrame } from 'react-native-safe-area-context' import { H5 } from '../Global/helpers/text' @@ -13,16 +12,16 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { BaseStackParamList } from '@/src/screens/types' import IconButton from '../Global/helpers/icon-button' import { fetchAlbumDiscs } from '../../api/queries/item' -import { useJellifyContext } from '../../providers' import { useLoadNewQueue } from '../../providers/Player/hooks/mutations' import { QueuingType } from '../../enums/queuing-type' import { useNetworkStatus } from '../../stores/network' import useStreamingDeviceProfile from '../../stores/device-profile' +import { useApi } from '../../stores' export default function ArtistHeader(): React.JSX.Element { const { width } = useSafeAreaFrame() - const { api } = useJellifyContext() + const api = useApi() const { artist, albums } = useArtistContext() diff --git a/src/components/Context/index.tsx b/src/components/Context/index.tsx index f322519c..7e8bb6f1 100644 --- a/src/components/Context/index.tsx +++ b/src/components/Context/index.tsx @@ -12,7 +12,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../enums/query-keys' import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item' -import { useJellifyContext } from '../../providers' import { getItemsApi } from '@jellyfin/sdk/lib/utils/api' import { AddToQueueMutation } from '../../providers/Player/interfaces' import { QueuingType } from '../../enums/queuing-type' @@ -33,6 +32,7 @@ import { useIsDownloaded } from '../../api/queries/download' import { useDeleteDownloads } from '../../api/mutations/download' import useHapticFeedback from '../../hooks/use-haptic-feedback' import { Platform } from 'react-native' +import { useApi } from '../../stores' type StackNavigation = Pick, 'navigate' | 'dispatch'> @@ -51,7 +51,7 @@ export default function ItemContext({ downloadedMediaSourceInfo, stackNavigation, }: ContextProps): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const trigger = useHapticFeedback() @@ -187,7 +187,7 @@ function AddToPlaylistRow({ } function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const [networkStatus] = useNetworkStatus() @@ -246,7 +246,7 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele } function DownloadMenuRow({ items }: { items: BaseItemDto[] }): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const { addToDownloadQueue, pendingDownloads } = useNetworkContext() const useRemoveDownload = useDeleteDownloads() @@ -376,7 +376,7 @@ function ViewArtistMenuRow({ artistId: string | null | undefined stackNavigation: StackNavigation | undefined }): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const { data: artist } = useQuery({ queryKey: [QueryKeys.ArtistById, artistId], diff --git a/src/components/Discover/helpers/public-playlists.tsx b/src/components/Discover/helpers/public-playlists.tsx index 078fceeb..f3a79d9a 100644 --- a/src/components/Discover/helpers/public-playlists.tsx +++ b/src/components/Discover/helpers/public-playlists.tsx @@ -2,14 +2,13 @@ import { H5, View, XStack } from 'tamagui' import { useDiscoverContext } from '../../../providers/Discover' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import Icon from '../../Global/components/icon' -import { useJellifyContext } from '../../../providers' import HorizontalCardList from '../../Global/components/horizontal-list' import { ItemCard } from '../../Global/components/item-card' -import { H4 } from '../../Global/helpers/text' import { useSafeAreaFrame } from 'react-native-safe-area-context' import { useNavigation } from '@react-navigation/native' import DiscoverStackParamList from '../../../screens/Discover/types' import navigationRef from '../../../../navigation' +import { useJellifyServer } from '../../../stores' export default function PublicPlaylists() { const { @@ -23,7 +22,7 @@ export default function PublicPlaylists() { const navigation = useNavigation>() - const { server } = useJellifyContext() + const [server] = useJellifyServer() const { width } = useSafeAreaFrame() return ( diff --git a/src/components/Global/components/image.tsx b/src/components/Global/components/image.tsx index 9c24973e..fc497919 100644 --- a/src/components/Global/components/image.tsx +++ b/src/components/Global/components/image.tsx @@ -1,8 +1,7 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { isUndefined } from 'lodash' import { getTokenValue, Token, View } from 'tamagui' -import { useJellifyContext } from '../../../providers' -import { StyleSheet, ViewStyle } from 'react-native' +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' @@ -10,6 +9,7 @@ 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 { useApi } from '../../../stores' interface ItemImageProps { item: BaseItemDto @@ -30,9 +30,9 @@ export default function ItemImage({ height, testID, }: ItemImageProps): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() - const imageUrl = getItemImageUrl(api, item, type) + const imageUrl = useMemo(() => getItemImageUrl(api, item, type), [api, item.Id, type]) return api ? ( , 'navigate' | 'dispatch'> }): React.JSX.Element { - const { api, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() const { data, isFetching, refetch } = useQuery({ queryKey: [QueryKeys.InstantMix, item.Id!], diff --git a/src/components/Global/components/item-row.tsx b/src/components/Global/components/item-row.tsx index 47b6840d..0ce96cea 100644 --- a/src/components/Global/components/item-row.tsx +++ b/src/components/Global/components/item-row.tsx @@ -6,18 +6,16 @@ import { QueuingType } from '../../../enums/queuing-type' import { RunTimeTicks } from '../helpers/time-codes' import ItemImage from './image' import FavoriteIcon from './favorite-icon' -import { Gesture, GestureDetector } from 'react-native-gesture-handler' -import { runOnJS } from 'react-native-reanimated' import navigationRef from '../../../../navigation' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { BaseStackParamList } from '../../../screens/types' import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations' -import { useJellifyContext } from '../../../providers' import { useNetworkStatus } from '../../../stores/network' import useStreamingDeviceProfile from '../../../stores/device-profile' import useItemContext from '../../../hooks/use-item-context' -import { RouteProp, useNavigation, useRoute } from '@react-navigation/native' +import { RouteProp, useRoute } from '@react-navigation/native' import { useCallback } from 'react' +import { useApi } from '../../../stores' interface ItemRowProps { item: BaseItemDto @@ -44,7 +42,7 @@ export default function ItemRow({ navigation, onPress, }: ItemRowProps): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const [networkStatus] = useNetworkStatus() diff --git a/src/components/Global/components/library-selector.tsx b/src/components/Global/components/library-selector.tsx index b3e7d1ee..aa9b9e62 100644 --- a/src/components/Global/components/library-selector.tsx +++ b/src/components/Global/components/library-selector.tsx @@ -3,12 +3,12 @@ import { getToken, Spinner, ToggleGroup, YStack } from 'tamagui' import { H2, Text } from '../helpers/text' import Button from '../helpers/button' import { SafeAreaView } from 'react-native-safe-area-context' -import { useJellifyContext } from '../../../providers' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { QueryKeys } from '../../../enums/query-keys' import { fetchUserViews } from '../../../api/queries/libraries' import { useQuery } from '@tanstack/react-query' import Icon from './icon' +import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores' interface LibrarySelectorProps { onLibrarySelected: ( @@ -37,7 +37,9 @@ export default function LibrarySelector({ showCancelButton = true, isOnboarding = false, }: LibrarySelectorProps): React.JSX.Element { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const { data: libraries, diff --git a/src/components/Global/components/track.tsx b/src/components/Global/components/track.tsx index b442cbee..577b7094 100644 --- a/src/components/Global/components/track.tsx +++ b/src/components/Global/components/track.tsx @@ -15,10 +15,10 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { BaseStackParamList } from '../../../screens/types' import ItemImage from './image' import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations' -import { useJellifyContext } from '../../../providers' import useStreamingDeviceProfile from '../../../stores/device-profile' import useStreamedMediaInfo from '../../../api/queries/media' import { useDownloadedTrack } from '../../../api/queries/download' +import { useApi } from '../../../stores' import { useCurrentTrack, usePlayQueue } from '../../../stores/player/queue' export interface TrackProps { @@ -55,7 +55,7 @@ export default function Track({ }: TrackProps): React.JSX.Element { const theme = useTheme() - const { api } = useJellifyContext() + const api = useApi() const deviceProfile = useStreamingDeviceProfile() diff --git a/src/components/Home/helpers/frequent-tracks.tsx b/src/components/Home/helpers/frequent-tracks.tsx index c2b44c8b..680daa3d 100644 --- a/src/components/Home/helpers/frequent-tracks.tsx +++ b/src/components/Home/helpers/frequent-tracks.tsx @@ -5,18 +5,17 @@ import { ItemCard } from '../../../components/Global/components/item-card' import { QueuingType } from '../../../enums/queuing-type' import Icon from '../../Global/components/icon' import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations' -import { H4 } from '../../../components/Global/helpers/text' import { useDisplayContext } from '../../../providers/Display/display-provider' import HomeStackParamList from '../../../screens/Home/types' import { useNavigation } from '@react-navigation/native' import { RootStackParamList } from '../../../screens/types' -import { useJellifyContext } from '../../../providers' import { useNetworkStatus } from '../../../stores/network' import useStreamingDeviceProfile from '../../../stores/device-profile' import { useFrequentlyPlayedTracks } from '../../../api/queries/frequents' +import { useApi } from '../../../stores' export default function FrequentlyPlayedTracks(): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const [networkStatus] = useNetworkStatus() diff --git a/src/components/Home/helpers/recently-played.tsx b/src/components/Home/helpers/recently-played.tsx index 4409f3b2..f2ea49b3 100644 --- a/src/components/Home/helpers/recently-played.tsx +++ b/src/components/Home/helpers/recently-played.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from 'react' import { H5, View, XStack } from 'tamagui' -import { H4 } from '../../Global/helpers/text' import { ItemCard } from '../../Global/components/item-card' import { RootStackParamList } from '../../../screens/types' import { NativeStackNavigationProp } from '@react-navigation/native-stack' @@ -11,14 +10,14 @@ import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations' import { useDisplayContext } from '../../../providers/Display/display-provider' import { useNavigation } from '@react-navigation/native' import HomeStackParamList from '../../../screens/Home/types' -import { useJellifyContext } from '../../../providers' import { useNetworkStatus } from '../../../stores/network' import useStreamingDeviceProfile from '../../../stores/device-profile' import { useRecentlyPlayedTracks } from '../../../api/queries/recents' import { useCurrentTrack } from '../../../stores/player/queue' +import { useApi } from '../../../stores' export default function RecentlyPlayed(): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const [networkStatus] = useNetworkStatus() diff --git a/src/components/Player/components/song-info.tsx b/src/components/Player/components/song-info.tsx index 9e071264..08e61b4e 100644 --- a/src/components/Player/components/song-info.tsx +++ b/src/components/Player/components/song-info.tsx @@ -6,7 +6,6 @@ import React, { useCallback, useMemo } from 'react' import ItemImage from '../../Global/components/image' import { useQuery } from '@tanstack/react-query' import { fetchItem } from '../../../api/queries/item' -import { useJellifyContext } from '../../../providers' import FavoriteButton from '../../Global/components/favorite-button' import { QueryKeys } from '../../../enums/query-keys' import navigationRef from '../../../../navigation' @@ -14,9 +13,10 @@ import Icon from '../../Global/components/icon' import { getItemName } from '../../../utils/text' import { CommonActions } from '@react-navigation/native' import { useCurrentTrack } from '../../../stores/player/queue' +import { useApi } from '../../../stores' export default function SongInfo(): React.JSX.Element { - const { api } = useJellifyContext() + const api = useApi() const nowPlaying = useCurrentTrack() const { data: album } = useQuery({ diff --git a/src/components/Playlist/components/header.tsx b/src/components/Playlist/components/header.tsx index d321453f..e48f83d3 100644 --- a/src/components/Playlist/components/header.tsx +++ b/src/components/Playlist/components/header.tsx @@ -1,15 +1,12 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { useSafeAreaFrame } from 'react-native-safe-area-context' -import { getToken, getTokens, Separator, View, XStack, YStack } from 'tamagui' +import { getTokens, Separator, View, XStack, YStack } from 'tamagui' import { AnimatedH5 } from '../../Global/helpers/text' import InstantMixButton from '../../Global/components/instant-mix-button' import Icon from '../../Global/components/icon' import { usePlaylistContext } from '../../../providers/Playlist' import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated' -import { getImageApi } from '@jellyfin/sdk/lib/utils/api' -import { useJellifyContext } from '../../../providers' -import { ImageType } from '@jellyfin/sdk/lib/generated-client/models' import { useNetworkStatus } from '../../../../src/stores/network' import { useNetworkContext } from '../../../../src/providers/Network' import { ActivityIndicator } from 'react-native' @@ -17,12 +14,12 @@ import { mapDtoToTrack } from '../../../utils/mappings' import { QueuingType } from '../../../enums/queuing-type' import { useNavigation } from '@react-navigation/native' import LibraryStackParamList from '@/src/screens/Library/types' -import { NitroImage } from 'react-native-nitro-image' import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations' import useStreamingDeviceProfile, { useDownloadingDeviceProfile, } from '../../../stores/device-profile' import ItemImage from '../../Global/components/image' +import { useApi } from '../../../stores' export default function PlayliistTracklistHeader( playlist: BaseItemDto, @@ -136,7 +133,7 @@ function PlaylistHeaderControls({ const downloadingDeviceProfile = useDownloadingDeviceProfile() const loadNewQueue = useLoadNewQueue() const isDownloading = pendingDownloads.length != 0 - const { api } = useJellifyContext() + const api = useApi() const [networkStatus] = useNetworkStatus() diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 0fc35e08..4c576956 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react' import Input from '../Global/helpers/input' import ItemRow from '../Global/components/item-row' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { RootStackParamList } from '../../screens/types' import { QueryKeys } from '../../enums/query-keys' import { fetchSearchResults } from '../../api/queries/search' import { useQuery } from '@tanstack/react-query' @@ -13,14 +12,16 @@ import Suggestions from './suggestions' import { isEmpty } from 'lodash' import HorizontalCardList from '../Global/components/horizontal-list' import { ItemCard } from '../Global/components/item-card' -import { useJellifyContext } from '../../providers' import SearchParamList from '../../screens/Search/types' +import { useApi, useJellifyLibrary, useJellifyUser } from '../../stores' export default function Search({ navigation, }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const { api, library, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const [searchString, setSearchString] = useState(undefined) diff --git a/src/components/Settings/components/account-tab.tsx b/src/components/Settings/components/account-tab.tsx index 1419d858..568a3b5e 100644 --- a/src/components/Settings/components/account-tab.tsx +++ b/src/components/Settings/components/account-tab.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { useJellifyContext } from '../../../providers' import SignOut from './sign-out-button' import { SettingsStackParamList } from '../../../screens/Settings/types' import { NativeStackNavigationProp } from '@react-navigation/native-stack' @@ -7,9 +6,12 @@ import { useNavigation } from '@react-navigation/native' import { Text } from '../../Global/helpers/text' import SettingsListGroup from './settings-list-group' import HTTPS from '../../../constants/protocols' +import { useJellifyUser, useJellifyLibrary, useJellifyServer } from '../../../stores' export default function AccountTab(): React.JSX.Element { - const { user, library, server } = useJellifyContext() + const [server] = useJellifyServer() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const navigation = useNavigation>() diff --git a/src/components/jellify.tsx b/src/components/jellify.tsx index ef3cbddb..fd295ad1 100644 --- a/src/components/jellify.tsx +++ b/src/components/jellify.tsx @@ -2,7 +2,6 @@ import _ from 'lodash' import React, { useEffect } from 'react' import Root from '../screens' import { PlayerProvider } from '../providers/Player' -import { JellifyProvider, useJellifyContext } from '../providers' import { NetworkContextProvider } from '../providers/Network' import { DisplayProvider } from '../providers/Display/display-provider' import { @@ -34,9 +33,7 @@ export default function Jellify(): React.JSX.Element { - - - + @@ -64,7 +61,7 @@ function JellifyLoggingWrapper({ children }: { children: React.ReactNode }): Rea } /** - * The main component for the Jellify app. Depends on {@link useJellifyContext} hook to determine if the user is logged in + * The main component for the Jellify app * @returns The {@link App} component */ function App(): React.JSX.Element { @@ -81,7 +78,6 @@ function App(): React.JSX.Element { return ( - diff --git a/src/hooks/use-item-context.ts b/src/hooks/use-item-context.ts index deff5c0b..914c342e 100644 --- a/src/hooks/use-item-context.ts +++ b/src/hooks/use-item-context.ts @@ -7,14 +7,15 @@ import { fetchMediaInfo } from '../api/queries/media/utils' import { fetchAlbumDiscs, fetchItem } from '../api/queries/item' import { getItemsApi } from '@jellyfin/sdk/lib/utils/api' import fetchUserData from '../api/queries/user-data/utils' -import { useJellifyContext } from '../providers' import { useCallback, useRef } from 'react' import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../stores/device-profile' import UserDataQueryKey from '../api/queries/user-data/keys' import MediaInfoQueryKey from '../api/queries/media/keys' +import { useApi, useJellifyUser } from '../stores' export default function useItemContext(): (item: BaseItemDto) => void { - const { api, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() const streamingDeviceProfile = useStreamingDeviceProfile() diff --git a/src/providers/Album/index.tsx b/src/providers/Album/index.tsx index dec19599..6dfe0fab 100644 --- a/src/providers/Album/index.tsx +++ b/src/providers/Album/index.tsx @@ -1,9 +1,9 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { QueryKeys } from '../../enums/query-keys' -import { useJellifyContext } from '..' import { fetchAlbumDiscs } from '../../api/queries/item' import { useQuery } from '@tanstack/react-query' import { createContext, ReactNode, useContext } from 'react' +import { useApi } from '../../stores' interface AlbumContext { album: BaseItemDto @@ -12,7 +12,7 @@ interface AlbumContext { } function AlbumContextInitializer(album: BaseItemDto): AlbumContext { - const { api } = useJellifyContext() + const api = useApi() const { data: discs, isPending } = useQuery({ queryKey: [QueryKeys.ItemTracks, album.Id], diff --git a/src/providers/Artist/index.tsx b/src/providers/Artist/index.tsx index 228f7d69..5c610a84 100644 --- a/src/providers/Artist/index.tsx +++ b/src/providers/Artist/index.tsx @@ -4,9 +4,9 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { useQuery } from '@tanstack/react-query' import { createContext, ReactNode, useCallback, useContext, useMemo } from 'react' import { SharedValue, useSharedValue } from 'react-native-reanimated' -import { useJellifyContext } from '..' import { isUndefined } from 'lodash' import { useArtistAlbums, useArtistFeaturedOn } from '../../api/queries/artist' +import { useApi, useJellifyUser, useJellifyLibrary } from '../../stores' interface ArtistContext { fetchingAlbums: boolean @@ -39,7 +39,9 @@ export const ArtistProvider = ({ artist: BaseItemDto children: ReactNode }) => { - const { api, library, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const { data: albums, diff --git a/src/providers/CarPlay/index.tsx b/src/providers/CarPlay/index.tsx index 5d1a459b..caf0c42d 100644 --- a/src/providers/CarPlay/index.tsx +++ b/src/providers/CarPlay/index.tsx @@ -2,17 +2,18 @@ import CarPlayNavigation from '../../components/CarPlay/Navigation' import { createContext, useEffect, useState } from 'react' import { Platform } from 'react-native' import { CarPlay } from 'react-native-carplay' -import { useJellifyContext } from '../index' import { useLoadNewQueue } from '../Player/hooks/mutations' import { useNetworkStatus } from '../../stores/network' import useStreamingDeviceProfile from '../../stores/device-profile' +import useJellifyStore, { useApi, useJellifyLibrary } from '../../stores' interface CarPlayContext { carplayConnected: boolean } const CarPlayContextInitializer = () => { - const { api, user, library } = useJellifyContext() + const api = useApi() + const [library] = useJellifyLibrary() const [carplayConnected, setCarPlayConnected] = useState(CarPlay ? CarPlay.connected : false) const [networkStatus] = useNetworkStatus() @@ -25,13 +26,13 @@ const CarPlayContextInitializer = () => { function onConnect() { setCarPlayConnected(true) - if (api && library) { + if (library) { CarPlay.setRootTemplate( CarPlayNavigation( library, loadNewQueue, api, - user, + useJellifyStore.getState().user, networkStatus, deviceProfile, ), diff --git a/src/providers/Discover/index.tsx b/src/providers/Discover/index.tsx index b235368d..fcae88d0 100644 --- a/src/providers/Discover/index.tsx +++ b/src/providers/Discover/index.tsx @@ -1,5 +1,4 @@ import { - InfiniteData, InfiniteQueryObserverResult, useInfiniteQuery, UseInfiniteQueryResult, @@ -7,10 +6,10 @@ import { import { QueryKeys } from '../../enums/query-keys' import { createContext, ReactNode, useContext, useState } from 'react' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' -import { useJellifyContext } from '..' import { fetchPublicPlaylists } from '../../api/queries/playlist/utils' import { fetchArtistSuggestions } from '../../api/queries/suggestions' import { useRefetchRecentlyAdded } from '../../api/queries/album' +import { useApi, useJellifyUser, useJellifyLibrary } from '../../stores' interface DiscoverContext { refreshing: boolean @@ -25,7 +24,9 @@ interface DiscoverContext { } const DiscoverContextInitializer = () => { - const { api, library, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const [refreshing, setRefreshing] = useState(false) const refetchRecentlyAdded = useRefetchRecentlyAdded() diff --git a/src/providers/Player/index.tsx b/src/providers/Player/index.tsx index afa9ed00..05f3d11a 100644 --- a/src/providers/Player/index.tsx +++ b/src/providers/Player/index.tsx @@ -7,7 +7,6 @@ import { useAutoDownload } from '../../stores/settings/usage' import reportPlaybackStopped from '../../api/mutations/playback/functions/playback-stopped' import reportPlaybackCompleted from '../../api/mutations/playback/functions/playback-completed' import isPlaybackFinished from '../../api/mutations/playback/utils' -import { useJellifyContext } from '..' import reportPlaybackProgress from '../../api/mutations/playback/functions/playback-progress' import reportPlaybackStarted from '../../api/mutations/playback/functions/playback-started' import calculateTrackVolume from './utils/normalization' @@ -15,6 +14,7 @@ import saveAudioItem from '../../api/mutations/download/utils' import { useDownloadingDeviceProfile } from '../../stores/device-profile' import Initialize from './functions/initialization' import { useEnableAudioNormalization } from '../../stores/settings/player' +import { useApi } from '../../stores' import { usePlayerQueueStore } from '../../stores/player/queue' const PLAYER_EVENTS: Event[] = [ @@ -28,7 +28,7 @@ interface PlayerContext {} export const PlayerContext = createContext({}) export const PlayerProvider: () => React.JSX.Element = () => { - const { api } = useJellifyContext() + const api = useApi() const [initialized, setInitialized] = useState(false) diff --git a/src/providers/Playlist/index.tsx b/src/providers/Playlist/index.tsx index 3163c31c..a543907a 100644 --- a/src/providers/Playlist/index.tsx +++ b/src/providers/Playlist/index.tsx @@ -1,11 +1,11 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { useMutation, UseMutationResult } from '@tanstack/react-query' -import { useJellifyContext } from '..' import { createContext, ReactNode, useContext, useEffect, useState } from 'react' import { removeFromPlaylist, updatePlaylist } from '../../api/mutations/playlists' import { RemoveFromPlaylistMutation } from '../../components/Playlist/interfaces' import { SharedValue, useSharedValue } from 'react-native-reanimated' import useHapticFeedback from '../../hooks/use-haptic-feedback' +import { useApi } from '../../stores' import { usePlaylistTracks } from '../../api/queries/playlist' interface PlaylistContext { @@ -30,7 +30,7 @@ interface PlaylistContext { } const PlaylistContextInitializer = (playlist: BaseItemDto) => { - const { api } = useJellifyContext() + const api = useApi() const canEdit = playlist.CanDelete const [editing, setEditing] = useState(false) diff --git a/src/providers/index.tsx b/src/providers/index.tsx deleted file mode 100644 index 1be4d8cf..00000000 --- a/src/providers/index.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { isUndefined } from 'lodash' -import { - createContext, - ReactNode, - SetStateAction, - useContext, - useEffect, - useState, - useMemo, -} from 'react' -import { JellifyLibrary } from '../types/JellifyLibrary' -import { JellifyServer } from '../types/JellifyServer' -import { JellifyUser } from '../types/JellifyUser' -import { storage } from '../constants/storage' -import { MMKVStorageKeys } from '../enums/mmkv-storage-keys' -import { Api } from '@jellyfin/sdk/lib/api' -import { JellyfinInfo } from '../api/info' -import { queryClient } from '../constants/query-client' -import AXIOS_INSTANCE from '../configs/axios.config' -import useAppActive from '../hooks/use-app-active' -import usePostFullCapabilities from '../api/mutations/session' - -/** - * The context for the Jellify provider. - */ -interface JellifyContext { - /** - * Whether the user is logged in. - */ - loggedIn: boolean - - /** - * The {@link Api} client. - */ - api: Api | undefined - - /** - * The connected {@link JellifyServer} object. - */ - server: JellifyServer | undefined - - /** - * The signed in {@link JellifyUser} object. - */ - user: JellifyUser | undefined - - /** - * The selected{@link JellifyLibrary} object. - */ - library: JellifyLibrary | undefined - - /** - * The function to set the context {@link JellifyServer}. - */ - setServer: React.Dispatch> - - /** - * The function to set the context {@link JellifyUser}. - */ - setUser: React.Dispatch> - - /** - * The function to set the context {@link JellifyLibrary}. - */ - setLibrary: React.Dispatch> - - /** - * The function to sign out of Jellify. This will clear the context - * and remove all data from the device. - */ - signOut: () => void -} - -const JellifyContextInitializer = () => { - const userJson = storage.getString(MMKVStorageKeys.User) - const serverJson = storage.getString(MMKVStorageKeys.Server) - const libraryJson = storage.getString(MMKVStorageKeys.Library) - const apiJson = storage.getString(MMKVStorageKeys.Api) - - const appIsActive = useAppActive() - - const [api, setApi] = useState(apiJson ? JSON.parse(apiJson) : undefined) - const [server, setServer] = useState( - serverJson ? JSON.parse(serverJson) : undefined, - ) - const [user, setUser] = useState( - userJson ? JSON.parse(userJson) : undefined, - ) - const [library, setLibrary] = useState( - libraryJson ? JSON.parse(libraryJson) : undefined, - ) - - const [loggedIn, setLoggedIn] = useState(false) - - const postFullCapabilities = usePostFullCapabilities() - - const signOut = () => { - setServer(undefined) - setUser(undefined) - setLibrary(undefined) - - queryClient.clear() - - storage.clearAll() - } - - useEffect(() => { - if (!isUndefined(server) && !isUndefined(user)) - setApi(JellyfinInfo.createApi(server.url, user.accessToken, AXIOS_INSTANCE)) - else if (!isUndefined(server)) - setApi(JellyfinInfo.createApi(server.url, undefined, AXIOS_INSTANCE)) - else setApi(undefined) - - setLoggedIn(!isUndefined(server) && !isUndefined(user) && !isUndefined(library)) - }, [server, user, library]) - - useEffect(() => { - if (api) storage.set(MMKVStorageKeys.Api, JSON.stringify(api)) - else storage.delete(MMKVStorageKeys.Api) - }, [api]) - - useEffect(() => { - if (server) storage.set(MMKVStorageKeys.Server, JSON.stringify(server)) - else storage.delete(MMKVStorageKeys.Server) - }, [server]) - - useEffect(() => { - if (user) storage.set(MMKVStorageKeys.User, JSON.stringify(user)) - else storage.delete(MMKVStorageKeys.User) - }, [user]) - - useEffect(() => { - if (library) storage.set(MMKVStorageKeys.Library, JSON.stringify(library)) - else storage.delete(MMKVStorageKeys.Library) - }, [library]) - - useEffect(() => { - if (appIsActive) postFullCapabilities.mutate(api) - }, [appIsActive, api]) - - return { - loggedIn, - api, - server, - user, - library, - setServer, - setUser, - setLibrary, - signOut, - } -} - -const JellifyContext = createContext({ - loggedIn: false, - api: undefined, - server: undefined, - user: undefined, - library: undefined, - setServer: () => {}, - setUser: () => {}, - setLibrary: () => {}, - signOut: () => {}, -}) - -/** - * Top level provider for Jellify. Provides the {@link JellifyContext} to all children, containing - * whether the user is logged in, and the {@link Api} client - * @param children The children to render - * @returns The {@link JellifyProvider} component - */ -export const JellifyProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({ - children, -}: { - children: ReactNode -}) => { - const context = JellifyContextInitializer() - - // Memoize the context value to prevent unnecessary re-renders - const value = useMemo( - () => context, - [ - context.loggedIn, - context.api, - context.server?.url, - context.user?.id, - context.library?.musicLibraryId, - ], - ) - - return {children} -} - -/** - * A hook to access the {@link JellifyContext} - * - * @returns The {@link JellifyContext} - */ -export const useJellifyContext = () => useContext(JellifyContext) diff --git a/src/screens/Library/add-playlist.tsx b/src/screens/Library/add-playlist.tsx index f469cfb6..11bd9808 100644 --- a/src/screens/Library/add-playlist.tsx +++ b/src/screens/Library/add-playlist.tsx @@ -7,18 +7,20 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { useMutation } from '@tanstack/react-query' import { createPlaylist } from '../../api/mutations/playlists' import Toast from 'react-native-toast-message' -import { useJellifyContext } from '../../providers' import Icon from '../../components/Global/components/icon' import LibraryStackParamList from './types' import useHapticFeedback from '../../hooks/use-haptic-feedback' import { useUserPlaylists } from '../../api/queries/playlist' +import { useApi, useJellifyUser, useJellifyLibrary } from '../../stores' export default function AddPlaylist({ navigation, }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const { api, user } = useJellifyContext() + const api = useApi() + const [user] = useJellifyUser() + const [library] = useJellifyLibrary() const [name, setName] = useState('') const { refetch } = useUserPlaylists() diff --git a/src/screens/Library/delete-playlist.tsx b/src/screens/Library/delete-playlist.tsx index 39966bb4..757ffd75 100644 --- a/src/screens/Library/delete-playlist.tsx +++ b/src/screens/Library/delete-playlist.tsx @@ -1,21 +1,21 @@ import { View, XStack } from 'tamagui' import Button from '../../components/Global/helpers/button' -import { H5, Text } from '../../components/Global/helpers/text' +import { Text } from '../../components/Global/helpers/text' import { useMutation } from '@tanstack/react-query' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { deletePlaylist } from '../../api/mutations/playlists' import { queryClient } from '../../constants/query-client' import { QueryKeys } from '../../enums/query-keys' -import { useJellifyContext } from '../../providers' import Icon from '../../components/Global/components/icon' import { LibraryDeletePlaylistProps } from './types' import useHapticFeedback from '../../hooks/use-haptic-feedback' +import { useApi } from '../../stores' export default function DeletePlaylist({ navigation, route, }: LibraryDeletePlaylistProps): React.JSX.Element { - const { api, user, library } = useJellifyContext() + const api = useApi() const trigger = useHapticFeedback() diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 9e5b607e..b68fed16 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -3,8 +3,8 @@ import ServerAuthentication from './server-authentication' import ServerAddress from './server-address' import { createNativeStackNavigator } from '@react-navigation/native-stack' import ServerLibrary from './server-library' -import { useJellifyContext } from '../../providers' import { useMemo } from 'react' +import { useApi, useJellifyUser } from '../../stores' const LoginStack = createNativeStackNavigator() @@ -13,7 +13,8 @@ const LoginStack = createNativeStackNavigator() * @returns The login screen. */ export default function Login(): React.JSX.Element { - const { user, server } = useJellifyContext() + const [user] = useJellifyUser() + const [server] = useJellifyUser() const initialRouteName = useMemo(() => { if (isUndefined(server)) { diff --git a/src/screens/Login/server-address.tsx b/src/screens/Login/server-address.tsx index b4e5f216..fef88d39 100644 --- a/src/screens/Login/server-address.tsx +++ b/src/screens/Login/server-address.tsx @@ -7,7 +7,6 @@ import Button from '../../components/Global/helpers/button' import { SafeAreaView } from 'react-native-safe-area-context' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import Toast from 'react-native-toast-message' -import { useJellifyContext } from '../../providers' import Icon from '../../components/Global/components/icon' import { IS_MAESTRO_BUILD } from '../../configs/config' import { sleepify } from '../../utils/sleep' @@ -16,6 +15,7 @@ import { useSendMetricsSetting } from '../../stores/settings/app' import usePublicSystemInfo from '../../api/mutations/public-system-info' import HTTPS, { HTTP } from '../../constants/protocols' import { JellifyServer } from '@/src/types/JellifyServer' +import { useSignOut } from '../../stores' export default function ServerAddress({ navigation, @@ -29,7 +29,7 @@ export default function ServerAddress({ const [useHttps, setUseHttps] = useState(true) const [serverAddress, setServerAddress] = useState(undefined) - const { signOut } = useJellifyContext() + const signOut = useSignOut() const [sendMetrics, setSendMetrics] = useSendMetricsSetting() diff --git a/src/screens/Login/server-authentication.tsx b/src/screens/Login/server-authentication.tsx index c9d01053..34c7a90f 100644 --- a/src/screens/Login/server-authentication.tsx +++ b/src/screens/Login/server-authentication.tsx @@ -6,12 +6,12 @@ import Button from '../../components/Global/helpers/button' import { SafeAreaView } from 'react-native-safe-area-context' import Input from '../../components/Global/helpers/input' import Icon from '../../components/Global/components/icon' -import { useJellifyContext } from '../../providers' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import Toast from 'react-native-toast-message' import { IS_MAESTRO_BUILD } from '../../configs/config' import LoginStackParamList from './types' import useAuthenticateUserByName from '../../api/mutations/authentication' +import { useJellifyServer } from '../../stores' export default function ServerAuthentication({ navigation, @@ -21,7 +21,7 @@ export default function ServerAuthentication({ const [username, setUsername] = useState(undefined) const [password, setPassword] = React.useState(undefined) - const { server } = useJellifyContext() + const [server] = useJellifyServer() const { mutate: authenticateUserByName, isPending } = useAuthenticateUserByName({ onSuccess: () => { diff --git a/src/screens/Login/server-library.tsx b/src/screens/Login/server-library.tsx index 90204989..6980d0ad 100644 --- a/src/screens/Login/server-library.tsx +++ b/src/screens/Login/server-library.tsx @@ -1,18 +1,18 @@ import React from 'react' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { RootStackParamList } from '../types' -import { useJellifyContext } from '../../providers' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import LibrarySelector from '../../components/Global/components/library-selector' import LoginStackParamList from './types' import { useNavigation } from '@react-navigation/native' +import { useJellifyLibrary } from '../../stores' export default function ServerLibrary({ navigation, }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const { setLibrary } = useJellifyContext() + const [, setLibrary] = useJellifyLibrary() const rootNavigation = useNavigation>() diff --git a/src/screens/Settings/library-selection.tsx b/src/screens/Settings/library-selection.tsx index ef4c20f7..fa8d8942 100644 --- a/src/screens/Settings/library-selection.tsx +++ b/src/screens/Settings/library-selection.tsx @@ -1,65 +1,64 @@ -import React from 'react' +import React, { useCallback } from 'react' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { SettingsStackParamList } from './types' -import { useJellifyContext } from '../../providers' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { QueryKeys } from '../../enums/query-keys' import { queryClient } from '../../constants/query-client' import Toast from 'react-native-toast-message' import LibrarySelector from '../../components/Global/components/library-selector' +import { useJellifyLibrary } from '../../stores' export default function LibrarySelectionScreen({ navigation, }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const { library, setLibrary } = useJellifyContext() + const [library, setLibrary] = useJellifyLibrary() + + const handleLibrarySelected = useCallback( + (libraryId: string, selectedLibrary: BaseItemDto, playlistLibrary?: BaseItemDto) => { + // Don't proceed if the same library is selected + if (libraryId === library?.musicLibraryId) { + navigation.goBack() + return + } + + setLibrary({ + musicLibraryId: libraryId, + musicLibraryName: selectedLibrary.Name ?? 'No library name', + musicLibraryPrimaryImageId: selectedLibrary.ImageTags?.Primary, + playlistLibraryId: playlistLibrary?.Id, + playlistLibraryPrimaryImageId: playlistLibrary?.ImageTags?.Primary, + }) + + // Invalidate all library-related queries to refresh the data + queryClient.invalidateQueries({ queryKey: [QueryKeys.AllArtistsAlphabetical] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.AllAlbumsAlphabetical] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.AllTracks] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.AllAlbums] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.AllArtists] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.Playlists] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoritePlaylists] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoriteArtists] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoriteAlbums] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoriteTracks] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyPlayed] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyPlayedArtists] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.FrequentArtists] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.FrequentlyPlayed] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyAdded] }) + queryClient.invalidateQueries({ queryKey: [QueryKeys.RefreshHome] }) + + Toast.show({ + text1: 'Library changed', + text2: `Now using ${selectedLibrary.Name}`, + type: 'success', + }) - const handleLibrarySelected = ( - libraryId: string, - selectedLibrary: BaseItemDto, - playlistLibrary?: BaseItemDto, - ) => { - // Don't proceed if the same library is selected - if (libraryId === library?.musicLibraryId) { navigation.goBack() - return - } - - setLibrary({ - musicLibraryId: libraryId, - musicLibraryName: selectedLibrary.Name ?? 'No library name', - musicLibraryPrimaryImageId: selectedLibrary.ImageTags?.Primary, - playlistLibraryId: playlistLibrary?.Id, - playlistLibraryPrimaryImageId: playlistLibrary?.ImageTags?.Primary, - }) - - // Invalidate all library-related queries to refresh the data - queryClient.invalidateQueries({ queryKey: [QueryKeys.AllArtistsAlphabetical] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.AllAlbumsAlphabetical] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.AllTracks] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.AllAlbums] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.AllArtists] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.Playlists] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoritePlaylists] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoriteArtists] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoriteAlbums] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.FavoriteTracks] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyPlayed] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyPlayedArtists] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.FrequentArtists] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.FrequentlyPlayed] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.RecentlyAdded] }) - queryClient.invalidateQueries({ queryKey: [QueryKeys.RefreshHome] }) - - Toast.show({ - text1: 'Library changed', - text2: `Now using ${selectedLibrary.Name}`, - type: 'success', - }) - - navigation.goBack() - } + }, + [setLibrary], + ) const handleCancel = () => { navigation.goBack() diff --git a/src/screens/Settings/sign-out-modal.tsx b/src/screens/Settings/sign-out-modal.tsx index 5dcea994..371abee1 100644 --- a/src/screens/Settings/sign-out-modal.tsx +++ b/src/screens/Settings/sign-out-modal.tsx @@ -3,13 +3,13 @@ import { SignOutModalProps } from './types' import { H5, Text } from '../../components/Global/helpers/text' import Button from '../../components/Global/helpers/button' import Icon from '../../components/Global/components/icon' -import { useJellifyContext } from '../../providers' import { useResetQueue } from '../../providers/Player/hooks/mutations' import navigationRef from '../../../navigation' import { useClearAllDownloads } from '../../api/mutations/download' +import { useJellifyServer } from '../../stores' export default function SignOutModal({ navigation }: SignOutModalProps): React.JSX.Element { - const { server } = useJellifyContext() + const [server] = useJellifyServer() const { mutate: resetQueue } = useResetQueue() const clearDownloads = useClearAllDownloads() diff --git a/src/screens/index.tsx b/src/screens/index.tsx index af8a4fea..08fc5068 100644 --- a/src/screens/index.tsx +++ b/src/screens/index.tsx @@ -1,26 +1,26 @@ -import Player, { PlayerStack } from './Player' +import Player from './Player' import Tabs from './Tabs' import { RootStackParamList } from './types' -import { getToken, useTheme, YStack } from 'tamagui' -import { useJellifyContext } from '../providers' +import { useTheme, YStack } from 'tamagui' import Login from './Login' -import { createNativeStackNavigator, NativeStackHeaderProps } from '@react-navigation/native-stack' +import { createNativeStackNavigator } from '@react-navigation/native-stack' import Context from './Context' import { getItemName } from '../utils/text' import AddToPlaylistSheet from './AddToPlaylist' -import { Platform } from 'react-native' import TextTicker from 'react-native-text-ticker' import { TextTickerConfig } from '../components/Player/component.config' import { Text } from '../components/Global/helpers/text' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import AudioSpecsSheet from './Stats' +import { useApi, useJellifyLibrary } from '../stores' const RootStack = createNativeStackNavigator() export default function Root(): React.JSX.Element { const theme = useTheme() - const { api, library } = useJellifyContext() + const api = useApi() + const [library] = useJellifyLibrary() return ( diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 00000000..ba63a3b9 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,104 @@ +import { create } from 'zustand' +import { useShallow } from 'zustand/react/shallow' +import { JellifyLibrary } from '../types/JellifyLibrary' +import { JellifyServer } from '../types/JellifyServer' +import { JellifyUser } from '../types/JellifyUser' +import { createJSONStorage, devtools, persist } from 'zustand/middleware' +import { stateStorage, storage } from '../constants/storage' +import { MMKVStorageKeys } from '../enums/mmkv-storage-keys' +import { Api } from '@jellyfin/sdk' +import { useCallback, useMemo } from 'react' +import { JellyfinInfo } from '../api/info' +import AXIOS_INSTANCE from '../configs/axios.config' +import { queryClient } from '../constants/query-client' + +type JellifyStore = { + server: JellifyServer | undefined + setServer: (server: JellifyServer | undefined) => void + + user: JellifyUser | undefined + setUser: (user: JellifyUser | undefined) => void + + library: JellifyLibrary | undefined + setLibrary: (library: JellifyLibrary | undefined) => void +} + +const useJellifyStore = create()( + devtools( + persist( + (set, get) => ({ + server: storage.getString(MMKVStorageKeys.Server) + ? (JSON.parse(storage.getString(MMKVStorageKeys.Server)!) as JellifyServer) + : undefined, + + setServer: (server: JellifyServer | undefined) => set({ server }), + + user: storage.getString(MMKVStorageKeys.User) + ? (JSON.parse(storage.getString(MMKVStorageKeys.User)!) as JellifyUser) + : undefined, + + setUser: (user: JellifyUser | undefined) => set({ user }), + + library: storage.getString(MMKVStorageKeys.Library) + ? (JSON.parse(storage.getString(MMKVStorageKeys.Library)!) as JellifyLibrary) + : undefined, + + setLibrary: (library: JellifyLibrary | undefined) => set({ library }), + }), + { + name: 'jellify-context-storage', + storage: createJSONStorage(() => stateStorage), + }, + ), + ), +) + +export const useJellifyServer: () => [ + JellifyServer | undefined, + (user: JellifyServer | undefined) => void, +] = () => { + return useJellifyStore(useShallow((state) => [state.server, state.setServer] as const)) +} + +export const useJellifyUser: () => [ + user: JellifyUser | undefined, + setUser: (user: JellifyUser | undefined) => void, +] = () => { + return useJellifyStore(useShallow((state) => [state.user, state.setUser] as const)) +} + +export const useJellifyLibrary: () => [ + library: JellifyLibrary | undefined, + setLibrary: (library: JellifyLibrary | undefined) => void, +] = () => { + return useJellifyStore(useShallow((state) => [state.library, state.setLibrary] as const)) +} + +export const useApi: () => Api | undefined = () => { + const [serverUrl, userAccessToken] = useJellifyStore( + useShallow((state) => [state.server?.url, state.user?.accessToken] as const), + ) + + return useMemo(() => { + if (!serverUrl) return undefined + else return JellyfinInfo.createApi(serverUrl, userAccessToken, AXIOS_INSTANCE) + }, [serverUrl, userAccessToken]) +} + +export const useSignOut = () => { + const [setServer, setUser, setLibrary] = useJellifyStore( + useShallow((state) => [state.setServer, state.setUser, state.setLibrary]), + ) + + return useCallback(() => { + setServer(undefined) + setUser(undefined) + setLibrary(undefined) + + queryClient.clear() + + storage.clearAll() + }, [setServer, setUser, setLibrary]) +} + +export default useJellifyStore