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
This commit is contained in:
Violet Caulfield
2025-11-04 06:39:32 -06:00
committed by GitHub
parent 4b7b62478f
commit 18d8d0bb59
56 changed files with 344 additions and 475 deletions

View File

@@ -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()
}
}

View File

@@ -93,8 +93,6 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
CFE47DDB2EA56B0200EB6067 /* icons */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = icons;
sourceTree = "<group>";
};
@@ -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;

View File

@@ -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 (
<View>
<Text testID='api-base-path'>{server?.url}</Text>
<Text testID='user-name'>{user?.name}</Text>
<Text testID='library-name'>{library?.musicLibraryName}</Text>
</View>
)
}
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(
<QueryClientProvider client={queryClient}>
<JellifyProvider>
<JellifyConsumer />
</JellifyProvider>
,
</QueryClientProvider>,
)
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')
})
})

View File

@@ -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(
<QueryClientProvider client={queryClient}>
<JellifyProvider>
<PlayerProvider />
</JellifyProvider>
<PlayerProvider />
</QueryClientProvider>,
)
})

View File

@@ -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:

View File

@@ -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<AuthenticationResult>) => {
console.log(`Received auth response from server`)

View File

@@ -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<boolean, Error, { item: BaseItemDto; autoCached: boolean }, void>,
] = () => {
const { api } = useJellifyContext()
const api = useApi()
const { data: downloadedTracks, refetch } = useAllDownloadedTracks()

View File

@@ -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()

View File

@@ -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}`),

View File

@@ -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) =>

View File

@@ -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<Set<string>>,
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<Set<string>>(new Set<string>())
@@ -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({

View File

@@ -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<Set<string>>,
UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>,
] = () => {
const { api, user, library } = useJellifyContext()
const api = useApi()
const [user] = useJellifyUser()
const [library] = useJellifyLibrary()
const { isFavorites, sortDescending } = useLibrarySortAndFilterContext()

View File

@@ -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()

View File

@@ -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({

View File

@@ -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()

View File

@@ -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],

View File

@@ -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!],

View File

@@ -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
*/

View File

@@ -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()

View File

@@ -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<Set<string>>,
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()

View File

@@ -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),

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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<NativeStackNavigationProp<BaseStackParamList>, '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],

View File

@@ -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<NativeStackNavigationProp<DiscoverStackParamList>>()
const { server } = useJellifyContext()
const [server] = useJellifyServer()
const { width } = useSafeAreaFrame()
return (
<View>

View File

@@ -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 ? (
<Image

View File

@@ -5,9 +5,10 @@ import { useQuery } from '@tanstack/react-query'
import { fetchInstantMixFromItem } from '../../../api/queries/instant-mixes'
import Icon from './icon'
import { Spacer, Spinner } from 'tamagui'
import { useJellifyContext } from '../../../providers'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../../screens/types'
import { useApi, useJellifyUser } from '../../../stores'
export default function InstantMixButton({
item,
navigation,
@@ -15,7 +16,8 @@ export default function InstantMixButton({
item: BaseItemDto
navigation: Pick<NativeStackNavigationProp<BaseStackParamList>, '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!],

View File

@@ -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()

View File

@@ -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,

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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({

View File

@@ -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()

View File

@@ -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<SearchParamList, 'SearchScreen'>
}): React.JSX.Element {
const { api, library, user } = useJellifyContext()
const api = useApi()
const [user] = useJellifyUser()
const [library] = useJellifyLibrary()
const [searchString, setSearchString] = useState<string | undefined>(undefined)

View File

@@ -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<NativeStackNavigationProp<SettingsStackParamList>>()

View File

@@ -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 {
<Theme name={theme === 'system' ? (isDarkMode ? 'dark' : 'light') : theme}>
<JellifyLoggingWrapper>
<DisplayProvider>
<JellifyProvider>
<App />
</JellifyProvider>
<App />
</DisplayProvider>
</JellifyLoggingWrapper>
</Theme>
@@ -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 (
<NetworkContextProvider>
<PlayerProvider />
<CarPlayProvider />
<Root />
<Toast topOffset={getToken('$12')} config={JellifyToastConfig(theme)} />
</NetworkContextProvider>

View File

@@ -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()

View File

@@ -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],

View File

@@ -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,

View File

@@ -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,
),

View File

@@ -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<boolean>(false)
const refetchRecentlyAdded = useRefetchRecentlyAdded()

View File

@@ -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<PlayerContext>({})
export const PlayerProvider: () => React.JSX.Element = () => {
const { api } = useJellifyContext()
const api = useApi()
const [initialized, setInitialized] = useState<boolean>(false)

View File

@@ -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<boolean>(false)

View File

@@ -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<SetStateAction<JellifyServer | undefined>>
/**
* The function to set the context {@link JellifyUser}.
*/
setUser: React.Dispatch<SetStateAction<JellifyUser | undefined>>
/**
* The function to set the context {@link JellifyLibrary}.
*/
setLibrary: React.Dispatch<SetStateAction<JellifyLibrary | undefined>>
/**
* 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<Api | undefined>(apiJson ? JSON.parse(apiJson) : undefined)
const [server, setServer] = useState<JellifyServer | undefined>(
serverJson ? JSON.parse(serverJson) : undefined,
)
const [user, setUser] = useState<JellifyUser | undefined>(
userJson ? JSON.parse(userJson) : undefined,
)
const [library, setLibrary] = useState<JellifyLibrary | undefined>(
libraryJson ? JSON.parse(libraryJson) : undefined,
)
const [loggedIn, setLoggedIn] = useState<boolean>(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<JellifyContext>({
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 <JellifyContext.Provider value={value}>{children}</JellifyContext.Provider>
}
/**
* A hook to access the {@link JellifyContext}
*
* @returns The {@link JellifyContext}
*/
export const useJellifyContext = () => useContext(JellifyContext)

View File

@@ -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<LibraryStackParamList, 'AddPlaylist'>
}): React.JSX.Element {
const { api, user } = useJellifyContext()
const api = useApi()
const [user] = useJellifyUser()
const [library] = useJellifyLibrary()
const [name, setName] = useState<string>('')
const { refetch } = useUserPlaylists()

View File

@@ -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()

View File

@@ -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)) {

View File

@@ -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<boolean>(true)
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined)
const { signOut } = useJellifyContext()
const signOut = useSignOut()
const [sendMetrics, setSendMetrics] = useSendMetricsSetting()

View File

@@ -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<string | undefined>(undefined)
const [password, setPassword] = React.useState<string | undefined>(undefined)
const { server } = useJellifyContext()
const [server] = useJellifyServer()
const { mutate: authenticateUserByName, isPending } = useAuthenticateUserByName({
onSuccess: () => {

View File

@@ -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<LoginStackParamList>
}): React.JSX.Element {
const { setLibrary } = useJellifyContext()
const [, setLibrary] = useJellifyLibrary()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()

View File

@@ -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<SettingsStackParamList, 'LibrarySelection'>
}): 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()

View File

@@ -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()

View File

@@ -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<RootStackParamList>()
export default function Root(): React.JSX.Element {
const theme = useTheme()
const { api, library } = useJellifyContext()
const api = useApi()
const [library] = useJellifyLibrary()
return (
<RootStack.Navigator initialRouteName={api && library ? 'Tabs' : 'Login'}>

104
src/stores/index.ts Normal file
View File

@@ -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<JellifyStore>()(
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