mirror of
https://github.com/Jellify-Music/App.git
synced 2026-02-19 18:28:51 -06:00
Login Flow Backend Refactor, Haptic Feedback Setting Enablement (#502)
Refactors haptic feedback usage to use a hook that will determine based on the user input if a haptic feedback event should be triggered. This fixes the issue where users would get haptic feedback even if the toggle for reducing it was enabled Refactors login flow to use external hooks to clean up components and split display vs functional logic Fixes DNS lookups in login flow. Now when a hostname connection fails, Jellify will properly perform a DNS lookup to determine the proper IP address of the Jellyfin server This should address issues with users connecting over Tailscale or relying on a custom DNS entry configured locally * update react navigation
This commit is contained in:
@@ -41,10 +41,10 @@
|
||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||
"@react-native-picker/picker": "^2.11.1",
|
||||
"@react-native-vector-icons/material-design-icons": "^12.3.0",
|
||||
"@react-navigation/bottom-tabs": "^7.4.6",
|
||||
"@react-navigation/material-top-tabs": "^7.3.6",
|
||||
"@react-navigation/bottom-tabs": "^7.4.7",
|
||||
"@react-navigation/material-top-tabs": "^7.3.7",
|
||||
"@react-navigation/native": "^7.1.17",
|
||||
"@react-navigation/native-stack": "^7.3.25",
|
||||
"@react-navigation/native-stack": "^7.3.26",
|
||||
"@sentry/react-native": "6.17.0",
|
||||
"@shopify/flash-list": "^2.0.3",
|
||||
"@tamagui/config": "^1.132.23",
|
||||
|
||||
54
src/api/mutations/authentication/index.ts
Normal file
54
src/api/mutations/authentication/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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'
|
||||
|
||||
interface AuthenticateUserByNameMutation {
|
||||
onSuccess?: () => void
|
||||
onError?: () => void
|
||||
}
|
||||
|
||||
const useAuthenticateUserByName = ({ onSuccess, onError }: AuthenticateUserByNameMutation) => {
|
||||
const { api, setUser } = useJellifyContext()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (credentials: JellyfinCredentials) => {
|
||||
return await api!.authenticateUserByName(credentials.username, credentials.password)
|
||||
},
|
||||
onSuccess: async (authResult: AxiosResponse<AuthenticationResult>) => {
|
||||
console.log(`Received auth response from server`)
|
||||
if (isUndefined(authResult))
|
||||
return Promise.reject(new Error('Authentication result was empty'))
|
||||
|
||||
if (authResult.status >= 400 || isUndefined(authResult.data.AccessToken))
|
||||
return Promise.reject(new Error('Invalid credentials'))
|
||||
|
||||
if (isUndefined(authResult.data.User))
|
||||
return Promise.reject(new Error('Unable to login'))
|
||||
|
||||
console.log(`Successfully signed in to server`)
|
||||
|
||||
const user: JellifyUser = {
|
||||
id: authResult.data.User!.Id!,
|
||||
name: authResult.data.User!.Name!,
|
||||
accessToken: authResult.data.AccessToken as string,
|
||||
}
|
||||
|
||||
setUser(user)
|
||||
|
||||
if (onSuccess) onSuccess()
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error('An error occurred connecting to the Jellyfin instance', error)
|
||||
|
||||
if (onError) onError()
|
||||
},
|
||||
retry: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export default useAuthenticateUserByName
|
||||
@@ -1,100 +0,0 @@
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
import { http } from '../../components/Login/utils/constants'
|
||||
|
||||
import { Jellyfin } from '@jellyfin/sdk/lib/jellyfin'
|
||||
import { JellyfinInfo } from '../info'
|
||||
import { https } from '../../components/Login/utils/constants'
|
||||
import { PublicSystemInfo } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getIpAddressesForHostname } from 'react-native-dns-lookup'
|
||||
|
||||
/**
|
||||
* Attempts to connect to a Jellyfin server.
|
||||
*
|
||||
* @param serverAddress The server address to connect to.
|
||||
* @param useHttps Whether to use HTTPS.
|
||||
* @returns The public system info response.
|
||||
*/
|
||||
export function connectToServer(
|
||||
serverAddress: string,
|
||||
useHttps: boolean,
|
||||
): Promise<{
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: 'hostname' | 'ipAddress'
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!serverAddress) return reject(new Error('Server address was empty'))
|
||||
|
||||
const serverAddressContainsProtocol =
|
||||
serverAddress.includes(http) || serverAddress.includes(https)
|
||||
|
||||
const jellyfin = new Jellyfin(JellyfinInfo)
|
||||
|
||||
const api = jellyfin.createApi(
|
||||
`${serverAddressContainsProtocol ? '' : useHttps ? https : http}${serverAddress}`,
|
||||
)
|
||||
|
||||
const connectViaHostnamePromise = () =>
|
||||
new Promise<{
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: 'hostname'
|
||||
}>((resolve, reject) => {
|
||||
getSystemApi(api)
|
||||
.getPublicSystemInfo()
|
||||
.then((response) => {
|
||||
if (!response.data.Version)
|
||||
return reject(
|
||||
new Error(
|
||||
'Jellyfin instance did not respond to our hostname request',
|
||||
),
|
||||
)
|
||||
return resolve({
|
||||
publicSystemInfoResponse: response.data,
|
||||
connectionType: 'hostname',
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('An error occurred getting public system info', error)
|
||||
return reject(new Error('Unable to connect to Jellyfin via hostname'))
|
||||
})
|
||||
})
|
||||
|
||||
const connectViaLocalNetworkPromise = () =>
|
||||
new Promise<{
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: 'ipAddress'
|
||||
}>((resolve, reject) =>
|
||||
getIpAddressesForHostname(serverAddress.split(':')[0]).then((ipAddress) => {
|
||||
const ipAddressApi = jellyfin.createApi(
|
||||
`${serverAddressContainsProtocol ? '' : useHttps ? https : http}${ipAddress[0]}:${serverAddress.split(':')[1]}`,
|
||||
)
|
||||
getSystemApi(ipAddressApi)
|
||||
.getPublicSystemInfo()
|
||||
.then((response) => {
|
||||
if (!response.data.Version)
|
||||
return reject(
|
||||
new Error(
|
||||
'Jellyfin instance did not respond to our IP Address request',
|
||||
),
|
||||
)
|
||||
return resolve({
|
||||
publicSystemInfoResponse: response.data,
|
||||
connectionType: 'ipAddress',
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('An error occurred getting public system info', error)
|
||||
return reject(new Error('Unable to connect to Jellyfin via IP Address'))
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
connectViaHostnamePromise()
|
||||
.then((response) => resolve(response))
|
||||
.catch(() =>
|
||||
connectViaLocalNetworkPromise()
|
||||
.then((response) => resolve(response))
|
||||
.catch(reject),
|
||||
)
|
||||
})
|
||||
}
|
||||
55
src/api/mutations/public-system-info/index.ts
Normal file
55
src/api/mutations/public-system-info/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
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'
|
||||
|
||||
interface PublicSystemInfoMutation {
|
||||
serverAddress: string
|
||||
useHttps: boolean
|
||||
}
|
||||
|
||||
interface PublicSystemInfoHook {
|
||||
onSuccess?: (server: JellifyServer) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
const usePublicSystemInfo = ({ onSuccess, onError }: PublicSystemInfoHook) => {
|
||||
const { setServer } = useJellifyContext()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ serverAddress, useHttps }: PublicSystemInfoMutation) =>
|
||||
connectToServer(serverAddress!, useHttps),
|
||||
onSuccess: ({ publicSystemInfoResponse, connectionType }, { serverAddress, useHttps }) => {
|
||||
console.debug(`Got public system info response`)
|
||||
|
||||
if (!publicSystemInfoResponse.Version)
|
||||
throw new Error(`Jellyfin instance did not respond`)
|
||||
|
||||
const server: JellifyServer = {
|
||||
url:
|
||||
connectionType === 'hostname'
|
||||
? `${serverAddressContainsProtocol(serverAddress) ? '' : useHttps ? HTTPS : HTTP}${serverAddress!}`
|
||||
: publicSystemInfoResponse.LocalAddress!,
|
||||
address: serverAddress!,
|
||||
name: publicSystemInfoResponse.ServerName!,
|
||||
version: publicSystemInfoResponse.Version!,
|
||||
startUpComplete: publicSystemInfoResponse.StartupWizardCompleted!,
|
||||
}
|
||||
|
||||
setServer(server)
|
||||
|
||||
if (onSuccess) onSuccess(server)
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('An error occurred connecting to the Jellyfin instance', error)
|
||||
|
||||
setServer(undefined)
|
||||
|
||||
if (onError) onError(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default usePublicSystemInfo
|
||||
79
src/api/mutations/public-system-info/utils/index.ts
Normal file
79
src/api/mutations/public-system-info/utils/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
import { Jellyfin } from '@jellyfin/sdk/lib/jellyfin'
|
||||
import { JellyfinInfo } from '../../../info'
|
||||
import { PublicSystemInfo } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getIpAddressesForHostname } from 'react-native-dns-lookup'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import HTTPS, { HTTP } from '../../../../constants/protocols'
|
||||
|
||||
type ConnectionType = 'hostname' | 'ipAddress'
|
||||
|
||||
/**
|
||||
* Attempts to connect to a Jellyfin server.
|
||||
*
|
||||
* @param serverAddress The server address to connect to.
|
||||
* @param useHttps Whether to use HTTPS.
|
||||
* @returns The public system info response.
|
||||
*/
|
||||
export function connectToServer(
|
||||
serverAddress: string,
|
||||
useHttps: boolean,
|
||||
): Promise<{
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: ConnectionType
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!serverAddress) return reject(new Error('Server address was empty'))
|
||||
|
||||
const serverAddressContainsProtocol =
|
||||
serverAddress.includes(HTTP) || serverAddress.includes(HTTPS)
|
||||
|
||||
const jellyfin = new Jellyfin(JellyfinInfo)
|
||||
|
||||
const hostnameApi = jellyfin.createApi(
|
||||
`${serverAddressContainsProtocol ? '' : useHttps ? HTTPS : HTTP}${serverAddress}`,
|
||||
)
|
||||
|
||||
const connectViaIpAddress = () => {
|
||||
return getIpAddressesForHostname(serverAddress.split(':')[0])
|
||||
.then((ipAddress) => {
|
||||
const ipAddressApi = jellyfin.createApi(
|
||||
`${serverAddressContainsProtocol ? '' : useHttps ? HTTPS : HTTP}${ipAddress[0]}:${serverAddress.split(':')[1]}`,
|
||||
)
|
||||
return connect(ipAddressApi, `ipAddress`)
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error(`Unable to lookup IP Addresses for Hostname`)
|
||||
})
|
||||
}
|
||||
|
||||
return connect(hostnameApi, 'hostname')
|
||||
.then((response) => resolve(response))
|
||||
.catch(() =>
|
||||
connectViaIpAddress()
|
||||
.then((response) => resolve(response))
|
||||
.catch(reject),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function connect(api: Api, connectionType: ConnectionType) {
|
||||
return getSystemApi(api)
|
||||
.getPublicSystemInfo()
|
||||
.then((response) => {
|
||||
if (!response.data.Version)
|
||||
throw new Error(
|
||||
`Jellyfin instance did not respond to our ${connectionType} request`,
|
||||
)
|
||||
|
||||
return {
|
||||
publicSystemInfoResponse: response.data,
|
||||
connectionType,
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('An error occurred getting public system info', error)
|
||||
throw new Error(`Unable to connect to Jellyfin via ${connectionType}`)
|
||||
})
|
||||
}
|
||||
9
src/api/mutations/public-system-info/utils/parsing.ts
Normal file
9
src/api/mutations/public-system-info/utils/parsing.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import HTTPS, { HTTP } from '../../../../constants/protocols'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
export default function serverAddressContainsProtocol(serverAddress: string | undefined) {
|
||||
return (
|
||||
!isUndefined(serverAddress) &&
|
||||
(serverAddress.includes(HTTP) || serverAddress.includes(HTTPS))
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import QueryConfig from '../../api/queries/query.config'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useMemo } from 'react'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import {
|
||||
YStack,
|
||||
@@ -28,10 +27,13 @@ import ItemImage from '../Global/components/image'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../Player/component.config'
|
||||
import { getItemName } from '../../utils/text'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
|
||||
export default function AddToPlaylist({ track }: { track: BaseItemDto }): React.JSX.Element {
|
||||
const { api, user, library } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const {
|
||||
data: playlists,
|
||||
isPending: playlistsFetchPending,
|
||||
|
||||
@@ -25,7 +25,6 @@ import { StackActions } from '@react-navigation/native'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../Player/component.config'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { useAddToQueue } from '../../providers/Player/hooks/mutations'
|
||||
import { useNetworkStatus } from '../../stores/network'
|
||||
import { useNetworkContext } from '../../providers/Network'
|
||||
@@ -33,6 +32,7 @@ import { mapDtoToTrack } from '../../utils/mappings'
|
||||
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../../stores/device-profile'
|
||||
import { useIsDownloaded } from '../../api/queries/download'
|
||||
import { useDeleteDownloads } from '../../api/mutations/download'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
|
||||
type StackNavigation = Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
|
||||
|
||||
@@ -55,6 +55,8 @@ export default function ItemContext({
|
||||
|
||||
const { bottom } = useSafeAreaInsets()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const isArtist = item.Type === BaseItemKind.MusicArtist
|
||||
const isAlbum = item.Type === BaseItemKind.MusicAlbum
|
||||
const isTrack = item.Type === BaseItemKind.Audio
|
||||
|
||||
@@ -10,10 +10,9 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import { Text } from '../helpers/text'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { useReducedHapticsSetting } from '../../../stores/settings/app'
|
||||
import { UseInfiniteQueryResult, useMutation } from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
|
||||
const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
|
||||
/**
|
||||
@@ -31,9 +30,9 @@ export default function AZScroller({
|
||||
}: {
|
||||
onLetterSelect: (letter: string) => void
|
||||
}) {
|
||||
const { width, height } = useSafeAreaFrame()
|
||||
const { width } = useSafeAreaFrame()
|
||||
const theme = useTheme()
|
||||
const [reducedHaptics] = useReducedHapticsSetting()
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const overlayOpacity = useSharedValue(0)
|
||||
|
||||
@@ -122,7 +121,7 @@ export default function AZScroller({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!reducedHaptics && overlayLetter) trigger('impactLight')
|
||||
trigger('impactLight')
|
||||
}, [overlayLetter])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SizeTokens, XStack, Separator, Switch, styled, getToken } from 'tamagui'
|
||||
import { Label } from './text'
|
||||
import { useEffect } from 'react'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { usePreviousValue } from '../../../hooks/use-previous-value'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
|
||||
interface SwitchWithLabelProps {
|
||||
onCheckedChange: (value: boolean) => void
|
||||
@@ -22,6 +22,8 @@ export function SwitchWithLabel(props: SwitchWithLabelProps) {
|
||||
|
||||
const previousChecked = usePreviousValue(props.checked)
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
useEffect(() => {
|
||||
if (previousChecked !== props.checked) {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -6,14 +6,13 @@ import { useLibrarySortAndFilterContext } from '../../providers/Library/sorting-
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { useReducedHapticsSetting } from '../../stores/settings/app'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
|
||||
function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
const { isFavorites, setIsFavorites, isDownloaded, setIsDownloaded } =
|
||||
useLibrarySortAndFilterContext()
|
||||
|
||||
const [reducedHaptics] = useReducedHapticsSetting()
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
@@ -37,7 +36,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
<XStack
|
||||
flex={1}
|
||||
onPress={() => {
|
||||
if (!reducedHaptics) trigger('impactLight')
|
||||
trigger('impactLight')
|
||||
props.navigation.navigate('AddPlaylist')
|
||||
}}
|
||||
alignItems={'center'}
|
||||
@@ -51,7 +50,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
<XStack
|
||||
flex={1}
|
||||
onPress={() => {
|
||||
if (!reducedHaptics) trigger('impactLight')
|
||||
trigger('impactLight')
|
||||
setIsFavorites(!isUndefined(isFavorites) ? undefined : true)
|
||||
}}
|
||||
alignItems={'center'}
|
||||
@@ -72,7 +71,7 @@ function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
<XStack
|
||||
flex={1}
|
||||
onPress={() => {
|
||||
if (!reducedHaptics) trigger('impactLight')
|
||||
trigger('impactLight')
|
||||
setIsDownloaded(!isDownloaded)
|
||||
}}
|
||||
alignItems={'center'}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const http = 'http://'
|
||||
export const https = 'https://'
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
|
||||
import { HorizontalSlider } from '../../../components/Global/helpers/slider'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { XStack, YStack } from 'tamagui'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { useSeekTo } from '../../../providers/Player/hooks/mutations'
|
||||
@@ -11,7 +10,7 @@ import { ProgressMultiplier } from '../component.config'
|
||||
import { useNowPlaying, useProgress } from '../../../providers/Player/hooks/queries'
|
||||
import QualityBadge from './quality-badge'
|
||||
import { useDisplayAudioQualityBadge } from '../../../stores/settings/player'
|
||||
import { useReducedHapticsSetting } from '../../../stores/settings/app'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
|
||||
// Create a simple pan gesture
|
||||
const scrubGesture = Gesture.Pan().runOnJS(true)
|
||||
@@ -20,7 +19,8 @@ export default function Scrubber(): React.JSX.Element {
|
||||
const { mutate: seekTo, isPending: seekPending, mutateAsync: seekToAsync } = useSeekTo()
|
||||
const { data: nowPlaying } = useNowPlaying()
|
||||
const { width } = useSafeAreaFrame()
|
||||
const reducedHaptics = useReducedHapticsSetting()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
// Get progress from the track player with the specified update interval
|
||||
// We *don't* use the duration from this hook because it will have a value of "0"
|
||||
@@ -116,9 +116,7 @@ export default function Scrubber(): React.JSX.Element {
|
||||
},
|
||||
onSlideMove: (event: unknown, value: number) => {
|
||||
// Throttled haptic feedback for better performance
|
||||
if (!reducedHaptics) {
|
||||
trigger('clockTick')
|
||||
}
|
||||
trigger('clockTick')
|
||||
|
||||
// Update position with proper clamping
|
||||
const clampedValue = Math.max(0, Math.min(value, maxDuration))
|
||||
@@ -139,7 +137,7 @@ export default function Scrubber(): React.JSX.Element {
|
||||
})
|
||||
},
|
||||
}),
|
||||
[maxDuration, reducedHaptics, handleSeek, calculatedPosition, width],
|
||||
[maxDuration, handleSeek, calculatedPosition, width],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,6 @@ import { RootStackParamList } from '../../screens/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import DraggableFlatList, { RenderItemParams } from 'react-native-draggable-flatlist'
|
||||
import { Separator, XStack } from 'tamagui'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useLayoutEffect, useCallback, useMemo } from 'react'
|
||||
import JellifyTrack from '../../types/JellifyTrack'
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
} from '../../providers/Player/hooks/mutations'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
|
||||
export default function Queue({
|
||||
navigation,
|
||||
@@ -30,6 +30,8 @@ export default function Queue({
|
||||
const { mutate: reorderQueue } = useReorderQueue()
|
||||
const { mutate: skip } = useSkip()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Separator, XStack } from 'tamagui'
|
||||
import Track from '../Global/components/track'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import { PlaylistProps } from './interfaces'
|
||||
import PlayliistTracklistHeader from './components/header'
|
||||
@@ -11,6 +10,7 @@ import AnimatedDraggableFlatList from '../Global/components/animated-draggable-f
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { RootStackParamList } from '../../screens/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
|
||||
export default function Playlist({
|
||||
playlist,
|
||||
@@ -28,6 +28,8 @@ export default function Playlist({
|
||||
useRemoveFromPlaylist,
|
||||
} = usePlaylistContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
|
||||
|
||||
const scrollOffsetHandler = useAnimatedScrollHandler({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
import { https } from '../../Login/utils/constants'
|
||||
import HTTPS from '../../../constants/protocols'
|
||||
|
||||
export default function AccountTab(): React.JSX.Element {
|
||||
const { user, library, server } = useJellifyContext()
|
||||
@@ -35,8 +35,8 @@ export default function AccountTab(): React.JSX.Element {
|
||||
{
|
||||
title: server?.name ?? 'Untitled Server',
|
||||
subTitle: server?.version ?? 'Unknown Jellyfin Version',
|
||||
iconName: server?.url.includes(https) ? 'lock' : 'lock-open',
|
||||
iconColor: server?.url.includes(https) ? '$success' : '$borderColor',
|
||||
iconName: server?.url.includes(HTTPS) ? 'lock' : 'lock-open',
|
||||
iconColor: server?.url.includes(HTTPS) ? '$success' : '$borderColor',
|
||||
children: <Text>{server?.address ?? 'Unknown Server'}</Text>,
|
||||
},
|
||||
]}
|
||||
|
||||
5
src/constants/protocols.ts
Normal file
5
src/constants/protocols.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const HTTPS = 'https://'
|
||||
|
||||
export const HTTP = 'http://'
|
||||
|
||||
export default HTTPS
|
||||
16
src/hooks/use-haptic-feedback.ts
Normal file
16
src/hooks/use-haptic-feedback.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { HapticFeedbackTypes, trigger } from 'react-native-haptic-feedback'
|
||||
import { useReducedHapticsSetting } from '../stores/settings/app'
|
||||
|
||||
const useHapticFeedback: () => (
|
||||
type?: keyof typeof HapticFeedbackTypes | HapticFeedbackTypes,
|
||||
) => void = () => {
|
||||
const [reducedHaptics] = useReducedHapticsSetting()
|
||||
|
||||
return (type?: keyof typeof HapticFeedbackTypes | HapticFeedbackTypes) => {
|
||||
if (!reducedHaptics) {
|
||||
trigger(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default useHapticFeedback
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import TrackPlayer, { RepeatMode, State } from 'react-native-track-player'
|
||||
import { loadQueue, playInQueue, playNextInQueue } from '../functions/queue'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { previous, skip } from '../functions/controls'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from '../interfaces'
|
||||
@@ -25,6 +24,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { RootStackParamList } from '../../../screens/types'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAllDownloadedTracks } from '../../../api/queries/download'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
|
||||
const PLAYER_MUTATION_OPTIONS = {
|
||||
retry: false,
|
||||
@@ -60,11 +60,14 @@ export const useInitialization = () =>
|
||||
/**
|
||||
* A mutation to handle starting playback
|
||||
*/
|
||||
export const usePlay = () =>
|
||||
useMutation({
|
||||
export const usePlay = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
onMutate: () => trigger('impactLight'),
|
||||
mutationFn: TrackPlayer.play,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutation to handle toggling the playback state
|
||||
@@ -75,6 +78,8 @@ export const useTogglePlayback = () => {
|
||||
usePlayerEngineStore((state) => state.playerEngineData) === PlayerEngine.GOOGLE_CAST
|
||||
const remoteClient = useRemoteMediaClient()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
trigger('impactMedium')
|
||||
@@ -115,8 +120,10 @@ export const useTogglePlayback = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useToggleRepeatMode = () =>
|
||||
useMutation({
|
||||
export const useToggleRepeatMode = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
onMutate: () => trigger('impactLight'),
|
||||
mutationFn: async () => {
|
||||
const repeatMode = await TrackPlayer.getRepeatMode()
|
||||
@@ -134,6 +141,7 @@ export const useToggleRepeatMode = () =>
|
||||
},
|
||||
onSettled: invalidateRepeatMode,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A mutation to handle seeking to a specific position in the track
|
||||
@@ -143,6 +151,8 @@ export const useSeekTo = () => {
|
||||
usePlayerEngineStore((state) => state.playerEngineData) === PlayerEngine.GOOGLE_CAST
|
||||
const remoteClient = useRemoteMediaClient()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
onMutate: () => trigger('impactLight'),
|
||||
mutationFn: async (position: number) => {
|
||||
@@ -162,7 +172,9 @@ export const useSeekTo = () => {
|
||||
/**
|
||||
* A mutation to handle seeking to a specific position in the track
|
||||
*/
|
||||
const useSeekBy = () =>
|
||||
const useSeekBy = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
useMutation({
|
||||
mutationFn: async (seekSeconds: number) => {
|
||||
trigger('clockTick')
|
||||
@@ -170,10 +182,13 @@ const useSeekBy = () =>
|
||||
await TrackPlayer.seekBy(seekSeconds)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddToQueue = () => {
|
||||
const downloadedTracks = useAllDownloadedTracks().data
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: AddToQueueMutation) =>
|
||||
variables.queuingType === QueuingType.PlayingNext
|
||||
@@ -215,6 +230,8 @@ export const useLoadNewQueue = () => {
|
||||
|
||||
const { data: downloadedTracks } = useAllDownloadedTracks()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
onMutate: async () => {
|
||||
trigger('impactLight')
|
||||
@@ -253,8 +270,10 @@ export const usePrevious = () =>
|
||||
},
|
||||
})
|
||||
|
||||
export const useSkip = () =>
|
||||
useMutation({
|
||||
export const useSkip = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
onMutate: (index?: number | undefined) => {
|
||||
trigger('impactMedium')
|
||||
|
||||
@@ -271,9 +290,12 @@ export const useSkip = () =>
|
||||
console.error('Failed to skip to next track:', error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveFromQueue = () =>
|
||||
useMutation({
|
||||
export const useRemoveFromQueue = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
onMutate: () => trigger('impactMedium'),
|
||||
mutationFn: async (index: number) => TrackPlayer.remove([index]),
|
||||
onSuccess: async (data: void, index: number) => {
|
||||
@@ -284,9 +306,12 @@ export const useRemoveFromQueue = () =>
|
||||
},
|
||||
onSettled: refetchPlayerQueue,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveUpcomingTracks = () =>
|
||||
useMutation({
|
||||
export const useRemoveUpcomingTracks = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: TrackPlayer.removeUpcomingTracks,
|
||||
onSuccess: () => trigger('notificationSuccess'),
|
||||
onError: async (error: Error) => {
|
||||
@@ -295,9 +320,12 @@ export const useRemoveUpcomingTracks = () =>
|
||||
},
|
||||
onSettled: refetchPlayerQueue,
|
||||
})
|
||||
}
|
||||
|
||||
export const useReorderQueue = () =>
|
||||
useMutation({
|
||||
export const useReorderQueue = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ from, to }: QueueOrderMutation) => {
|
||||
console.debug(
|
||||
`TrackPlayer.move(${from}, ${to}) - Queue before move:`,
|
||||
@@ -310,7 +338,6 @@ export const useReorderQueue = () =>
|
||||
console.debug(`Reordering queue from ${from} to ${to}`)
|
||||
},
|
||||
onSuccess: async (_, { from, to }: { from: number; to: number }) => {
|
||||
trigger('notificationSuccess')
|
||||
console.debug(`Reordered queue from ${from} to ${to} successfully`)
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
@@ -319,6 +346,7 @@ export const useReorderQueue = () =>
|
||||
},
|
||||
onSettled: refetchPlayerQueue,
|
||||
})
|
||||
}
|
||||
|
||||
export const useResetQueue = () =>
|
||||
useMutation({
|
||||
@@ -331,8 +359,10 @@ export const useResetQueue = () =>
|
||||
onSettled: refetchPlayerQueue,
|
||||
})
|
||||
|
||||
export const useToggleShuffle = () =>
|
||||
useMutation({
|
||||
export const useToggleShuffle = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
onMutate: () => trigger('impactLight'),
|
||||
mutationFn: async (shuffled: boolean | undefined) =>
|
||||
shuffled ? await handleDeshuffle() : await handleShuffle(),
|
||||
@@ -345,6 +375,7 @@ export const useToggleShuffle = () =>
|
||||
},
|
||||
onSuccess: refetchPlayerQueue,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAudioNormalization = () =>
|
||||
useMutation({
|
||||
|
||||
@@ -4,10 +4,10 @@ import { QueryKeys } from '../../enums/query-keys'
|
||||
import { useJellifyContext } from '..'
|
||||
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
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'
|
||||
|
||||
interface PlaylistContext {
|
||||
playlist: BaseItemDto
|
||||
@@ -40,6 +40,8 @@ const PlaylistContextInitializer = (playlist: BaseItemDto) => {
|
||||
|
||||
const scroll = useSharedValue(0)
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const {
|
||||
data: tracks,
|
||||
isPending,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { createContext, ReactNode, SetStateAction, useContext } from 'react'
|
||||
import { createContext, ReactNode, useContext } from 'react'
|
||||
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useJellifyContext } from '..'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto
|
||||
@@ -20,6 +20,9 @@ interface JellifyUserDataContext {
|
||||
|
||||
const JellifyUserDataContextInitializer = () => {
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const useSetFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(api!).markFavoriteItem({
|
||||
|
||||
@@ -4,16 +4,15 @@ import React, { useState } from 'react'
|
||||
import { View, XStack } from 'tamagui'
|
||||
import Button from '../../components/Global/helpers/button'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { RootStackParamList } from '../types'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { createPlaylist } from '../../api/mutations/playlists'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
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'
|
||||
|
||||
export default function AddPlaylist({
|
||||
navigation,
|
||||
@@ -23,6 +22,8 @@ export default function AddPlaylist({
|
||||
const { api, user } = useJellifyContext()
|
||||
const [name, setName] = useState<string>('')
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const useAddPlaylist = useMutation({
|
||||
mutationFn: ({ name }: { name: string }) => createPlaylist(api, user, name),
|
||||
onSuccess: (data: void, { name }: { name: string }) => {
|
||||
|
||||
@@ -4,19 +4,21 @@ import { H5, 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 { trigger } from 'react-native-haptic-feedback'
|
||||
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 * as Burnt from 'burnt'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
|
||||
export default function DeletePlaylist({
|
||||
navigation,
|
||||
route,
|
||||
}: LibraryDeletePlaylistProps): React.JSX.Element {
|
||||
const { api, user, library } = useJellifyContext()
|
||||
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const useDeletePlaylist = useMutation({
|
||||
mutationFn: (playlist: BaseItemDto) => deletePlaylist(api, playlist.Id!),
|
||||
onSuccess: (data: void, playlist: BaseItemDto) => {
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import _, { isUndefined } from 'lodash'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { JellifyServer } from '../../types/JellifyServer'
|
||||
import { isEmpty, isUndefined } from 'lodash'
|
||||
import { Input, ListItem, Separator, Spinner, XStack, YGroup, YStack } from 'tamagui'
|
||||
import { SwitchWithLabel } from '../../components/Global/helpers/switch-with-label'
|
||||
import { H2, Text } from '../../components/Global/helpers/text'
|
||||
import Button from '../../components/Global/helpers/button'
|
||||
import { http, https } from '../../components/Login/utils/constants'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { RootStackParamList } from '../types'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import Icon from '../../components/Global/components/icon'
|
||||
import { PublicSystemInfo } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { connectToServer } from '../../api/mutations/login'
|
||||
import { IS_MAESTRO_BUILD } from '../../configs/config'
|
||||
import { sleepify } from '../../utils/sleep'
|
||||
import LoginStackParamList from './types'
|
||||
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'
|
||||
|
||||
export default function ServerAddress({
|
||||
navigation,
|
||||
@@ -32,65 +29,31 @@ export default function ServerAddress({
|
||||
const [useHttps, setUseHttps] = useState<boolean>(true)
|
||||
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined)
|
||||
|
||||
const { server, setServer, signOut } = useJellifyContext()
|
||||
const { signOut } = useJellifyContext()
|
||||
|
||||
const [sendMetrics, setSendMetrics] = useSendMetricsSetting()
|
||||
|
||||
useEffect(() => {
|
||||
setServerAddressContainsProtocol(
|
||||
!isUndefined(serverAddress) &&
|
||||
(serverAddress.includes(http) || serverAddress.includes(https)),
|
||||
(serverAddress.includes(HTTP) || serverAddress.includes(HTTPS)),
|
||||
)
|
||||
setServerAddressContainsHttps(!isUndefined(serverAddress) && serverAddress.includes(https))
|
||||
setServerAddressContainsHttps(!isUndefined(serverAddress) && serverAddress.includes(HTTPS))
|
||||
}, [serverAddress])
|
||||
|
||||
useEffect(() => {
|
||||
sleepify(1000).then(() => signOut())
|
||||
}, [])
|
||||
|
||||
const useServerMutation = useMutation({
|
||||
mutationFn: () => connectToServer(serverAddress!, useHttps),
|
||||
onSuccess: ({
|
||||
publicSystemInfoResponse,
|
||||
connectionType,
|
||||
}: {
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: 'hostname' | 'ipAddress'
|
||||
}) => {
|
||||
if (!publicSystemInfoResponse.Version)
|
||||
throw new Error('Jellyfin instance did not respond')
|
||||
|
||||
console.debug(`Connected to Jellyfin via ${connectionType}`, publicSystemInfoResponse)
|
||||
console.log(`Connected to Jellyfin ${publicSystemInfoResponse.Version!}`)
|
||||
|
||||
const server: JellifyServer = {
|
||||
url:
|
||||
connectionType === 'hostname'
|
||||
? `${serverAddressContainsProtocol ? '' : useHttps ? https : http}${serverAddress!}`
|
||||
: publicSystemInfoResponse.LocalAddress!,
|
||||
address: serverAddress!,
|
||||
name: publicSystemInfoResponse.ServerName!,
|
||||
version: publicSystemInfoResponse.Version!,
|
||||
startUpComplete: publicSystemInfoResponse.StartupWizardCompleted!,
|
||||
}
|
||||
|
||||
setServer(server)
|
||||
|
||||
const { mutate: connectToServer, isPending } = usePublicSystemInfo({
|
||||
onSuccess: (server: JellifyServer) => {
|
||||
navigation.navigate('ServerAuthentication')
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error('An error occurred connecting to the Jellyfin instance', error)
|
||||
setServer(undefined)
|
||||
|
||||
// Burnt.toast({
|
||||
// title: 'Unable to connect',
|
||||
// preset: 'error',
|
||||
// // message: `Unable to connect to Jellyfin at ${useHttps ? https : http}${serverAddress}`,
|
||||
// })
|
||||
onError: () => {
|
||||
Toast.show({
|
||||
text1: 'Unable to connect',
|
||||
text2: `Unable to connect to Jellyfin at ${
|
||||
serverAddressContainsProtocol ? '' : useHttps ? https : http
|
||||
text2: ` at ${
|
||||
serverAddressContainsProtocol ? '' : useHttps ? HTTPS : HTTP
|
||||
}${serverAddress}`,
|
||||
type: 'error',
|
||||
})
|
||||
@@ -121,7 +84,7 @@ export default function ServerAddress({
|
||||
textAlign='center'
|
||||
verticalAlign={'center'}
|
||||
>
|
||||
{useHttps ? https : http}
|
||||
{useHttps ? HTTPS : HTTP}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -200,13 +163,14 @@ export default function ServerAddress({
|
||||
</YGroup.Item>
|
||||
</YGroup>
|
||||
|
||||
{useServerMutation.isPending ? (
|
||||
{isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button
|
||||
disabled={_.isEmpty(serverAddress)}
|
||||
disabled={isEmpty(serverAddress)}
|
||||
onPress={() => {
|
||||
useServerMutation.mutate()
|
||||
if (!isUndefined(serverAddress))
|
||||
connectToServer({ serverAddress, useHttps })
|
||||
}}
|
||||
testID='connect_button'
|
||||
>
|
||||
|
||||
@@ -1,69 +1,37 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import _ from 'lodash'
|
||||
import { JellyfinCredentials } from '../../api/types/jellyfin-credentials'
|
||||
import { getToken, H6, Spacer, Spinner, XStack, YStack } from 'tamagui'
|
||||
import { H2, H5, Text } from '../../components/Global/helpers/text'
|
||||
import { H6, Spacer, Spinner, XStack, YStack } from 'tamagui'
|
||||
import { H2 } from '../../components/Global/helpers/text'
|
||||
import Button from '../../components/Global/helpers/button'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
import { RootStackParamList } from '../types'
|
||||
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 { AxiosResponse } from 'axios'
|
||||
import { AuthenticationResult } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import LoginStackParamList from './types'
|
||||
import useAuthenticateUserByName from '../../api/mutations/authentication'
|
||||
|
||||
export default function ServerAuthentication({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<LoginStackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { api } = useJellifyContext()
|
||||
const [username, setUsername] = useState<string | undefined>(undefined)
|
||||
const [password, setPassword] = React.useState<string | undefined>(undefined)
|
||||
|
||||
const { server, setUser, setServer } = useJellifyContext()
|
||||
|
||||
const useApiMutation = useMutation({
|
||||
mutationFn: async (credentials: JellyfinCredentials) => {
|
||||
return await api!.authenticateUserByName(credentials.username, credentials.password)
|
||||
},
|
||||
onSuccess: async (authResult: AxiosResponse<AuthenticationResult>) => {
|
||||
console.log(`Received auth response from server`)
|
||||
if (_.isUndefined(authResult))
|
||||
return Promise.reject(new Error('Authentication result was empty'))
|
||||
|
||||
if (authResult.status >= 400 || _.isEmpty(authResult.data.AccessToken))
|
||||
return Promise.reject(new Error('Invalid credentials'))
|
||||
|
||||
if (_.isUndefined(authResult.data.User))
|
||||
return Promise.reject(new Error('Unable to login'))
|
||||
|
||||
console.log(`Successfully signed in to server`)
|
||||
|
||||
const user: JellifyUser = {
|
||||
id: authResult.data.User!.Id!,
|
||||
name: authResult.data.User!.Name!,
|
||||
accessToken: authResult.data.AccessToken as string,
|
||||
}
|
||||
|
||||
setUser(user)
|
||||
const { server } = useJellifyContext()
|
||||
|
||||
const { mutate: authenticateUserByName, isPending } = useAuthenticateUserByName({
|
||||
onSuccess: () => {
|
||||
navigation.navigate('LibrarySelection')
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error('An error occurred connecting to the Jellyfin instance', error)
|
||||
|
||||
onError: () => {
|
||||
Toast.show({
|
||||
text1: `Unable to sign in to ${server!.name}`,
|
||||
type: 'error',
|
||||
})
|
||||
return Promise.reject(`An error occured signing into ${server!.name}`)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -79,7 +47,7 @@ export default function ServerAuthentication({
|
||||
</YStack>
|
||||
<YStack marginHorizontal={'$4'}>
|
||||
<Input
|
||||
prependElement={<Icon name='human-greeting-variant' color={'$borderColor'} />}
|
||||
prependElement={<Icon name='human-greeting-variant' color={'$primary'} />}
|
||||
placeholder='Username'
|
||||
value={username}
|
||||
style={
|
||||
@@ -95,7 +63,7 @@ export default function ServerAuthentication({
|
||||
<Spacer />
|
||||
|
||||
<Input
|
||||
prependElement={<Icon name='lock-outline' color={'$borderColor'} />}
|
||||
prependElement={<Icon name='lock-outline' color={'$primary'} />}
|
||||
placeholder='Password'
|
||||
value={password}
|
||||
testID='password_input'
|
||||
@@ -116,23 +84,23 @@ export default function ServerAuthentication({
|
||||
icon={() => <Icon name='chevron-left' small />}
|
||||
bordered={0}
|
||||
onPress={() => {
|
||||
navigation.navigate('ServerAddress', undefined, { pop: true })
|
||||
navigation.popTo('ServerAddress', undefined)
|
||||
}}
|
||||
>
|
||||
Switch Server
|
||||
</Button>
|
||||
{useApiMutation.isPending ? (
|
||||
{isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button
|
||||
marginVertical={0}
|
||||
disabled={_.isEmpty(username) || useApiMutation.isPending}
|
||||
disabled={_.isEmpty(username) || isPending}
|
||||
icon={() => <Icon name='chevron-right' small />}
|
||||
testID='sign_in_button'
|
||||
onPress={() => {
|
||||
if (!_.isUndefined(username)) {
|
||||
console.log(`Signing in...`)
|
||||
useApiMutation.mutate({ username, password })
|
||||
authenticateUserByName({ username, password })
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function ServerLibrary({
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<LoginStackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { setUser, setLibrary } = useJellifyContext()
|
||||
const { setLibrary } = useJellifyContext()
|
||||
|
||||
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
|
||||
|
||||
@@ -32,7 +32,6 @@ export default function ServerLibrary({
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setUser(undefined)
|
||||
navigation.navigate('ServerAuthentication', undefined, {
|
||||
pop: true,
|
||||
})
|
||||
|
||||
38
yarn.lock
38
yarn.lock
@@ -2133,12 +2133,12 @@
|
||||
invariant "^2.2.4"
|
||||
nullthrows "^1.1.1"
|
||||
|
||||
"@react-navigation/bottom-tabs@^7.4.6":
|
||||
version "7.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.6.tgz#a7c7b4347d9349babc1d481fc59f814dbe7d7222"
|
||||
integrity sha512-f4khxwcL70O5aKfZFbxyBo5RnzPFnBNSXmrrT7q9CRmvN4mHov9KFKGQ3H4xD5sLonsTBtyjvyvPfyEC4G7f+g==
|
||||
"@react-navigation/bottom-tabs@^7.4.7":
|
||||
version "7.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.7.tgz#c6fb80bfe25f47db27491918a764e01877f7efeb"
|
||||
integrity sha512-SQ4KuYV9yr3SV/thefpLWhAD0CU2CrBMG1l0w/QKl3GYuGWdN5OQmdQdmaPZGtsjjVOb+N9Qo7Tf6210P4TlpA==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^2.6.3"
|
||||
"@react-navigation/elements" "^2.6.4"
|
||||
color "^4.2.3"
|
||||
|
||||
"@react-navigation/core@^7.12.4":
|
||||
@@ -2154,30 +2154,30 @@
|
||||
use-latest-callback "^0.2.4"
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
||||
"@react-navigation/elements@^2.6.3":
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.6.3.tgz#77cc4d989c0831ec59dc87b982f18bc644ac8e67"
|
||||
integrity sha512-hcPXssZg5bFD5oKX7FP0D9ZXinRgPUHkUJbTegpenSEUJcPooH1qzWJkEP22GrtO+OPDLYrCVZxEX8FcMrn4pA==
|
||||
"@react-navigation/elements@^2.6.4":
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.6.4.tgz#f1dc8548b1289588fabcd2f0342c1391c689a49f"
|
||||
integrity sha512-O3X9vWXOEhAO56zkQS7KaDzL8BvjlwZ0LGSteKpt1/k6w6HONG+2Wkblrb057iKmehTkEkQMzMLkXiuLmN5x9Q==
|
||||
dependencies:
|
||||
color "^4.2.3"
|
||||
use-latest-callback "^0.2.4"
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
||||
"@react-navigation/material-top-tabs@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/material-top-tabs/-/material-top-tabs-7.3.6.tgz#39f7b37c948c24a65b5a5229f8d65e86a65e0403"
|
||||
integrity sha512-94r6euJ0VFnJ6Ixp4BWO9sTQjuh7dq6nEBirMRLqVZXMVZS6nsB2olw7cA8vWjQCXIM3nLNIa2t/hIzRH2yR6Q==
|
||||
"@react-navigation/material-top-tabs@^7.3.7":
|
||||
version "7.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/material-top-tabs/-/material-top-tabs-7.3.7.tgz#16e8efaf7bc3be89c2c1e5a4f48124149ec0db3a"
|
||||
integrity sha512-lVgcJON9kqCETE628/E5da0jbSB1QxHdYaKetxAIEFCgcANebcg1wU80P2B3PBFp2qRrLfrPS9oHTUfaUIjLSA==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^2.6.3"
|
||||
"@react-navigation/elements" "^2.6.4"
|
||||
color "^4.2.3"
|
||||
react-native-tab-view "^4.1.3"
|
||||
|
||||
"@react-navigation/native-stack@^7.3.25":
|
||||
version "7.3.25"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.3.25.tgz#426179dd10f90977480c7d7720f094ef64c840bb"
|
||||
integrity sha512-jGcgUpif0dDGwuqag6rKTdS78MiAVAy8vmQppyaAgjS05VbCfDX+xjhc8dUxSClO5CoWlDoby1c8Hw4kBfL2UA==
|
||||
"@react-navigation/native-stack@^7.3.26":
|
||||
version "7.3.26"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.3.26.tgz#a08ee0626e49428a808da9d810f24db5b08deae9"
|
||||
integrity sha512-EjaBWzLZ76HJGOOcWCFf+h/M+Zg7M1RalYioDOb6ZdXHz7AwYNidruT3OUAQgSzg3gVLqvu5OYO0jFsNDPCZxQ==
|
||||
dependencies:
|
||||
"@react-navigation/elements" "^2.6.3"
|
||||
"@react-navigation/elements" "^2.6.4"
|
||||
warn-once "^0.1.1"
|
||||
|
||||
"@react-navigation/native@^7.1.17":
|
||||
|
||||
Reference in New Issue
Block a user