diff --git a/assets/icons/teal-icon.png b/assets/icons/teal-icon.png new file mode 100644 index 00000000..3e257496 Binary files /dev/null and b/assets/icons/teal-icon.png differ diff --git a/assets/icons/teal-icon.svg b/assets/icons/teal-icon.svg deleted file mode 100644 index 197cc290..00000000 --- a/assets/icons/teal-icon.svg +++ /dev/null @@ -1,407 +0,0 @@ - - - - diff --git a/ios/Jellify.xcodeproj/project.pbxproj b/ios/Jellify.xcodeproj/project.pbxproj index bc4a7cf8..9fc3dfbe 100644 --- a/ios/Jellify.xcodeproj/project.pbxproj +++ b/ios/Jellify.xcodeproj/project.pbxproj @@ -93,8 +93,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ CFE47DDB2EA56B0200EB6067 /* icons */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = icons; sourceTree = ""; }; @@ -397,10 +395,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n"; @@ -414,10 +416,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n"; @@ -697,10 +703,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -787,10 +790,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/src/api/mutations/quickconnect/index.ts b/src/api/mutations/quickconnect/index.ts index 2cd61232..322f3df5 100644 --- a/src/api/mutations/quickconnect/index.ts +++ b/src/api/mutations/quickconnect/index.ts @@ -1,20 +1,13 @@ 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 { capitalize, isUndefined } from 'lodash' +import { isUndefined } from 'lodash' import { getQuickConnectApi, getUserApi } from '@jellyfin/sdk/lib/utils/api' import { useNavigation } from '@react-navigation/native' import LoginStackParamList from '@/src/screens/Login/types' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { getDeviceId, getDeviceNameSync, getUniqueIdSync } from 'react-native-device-info' -import { name, version } from '../../../../package.json' - -const QUICK_CONNECT_HEADER = encodeURIComponent( - `MediaBrowser Device='${getDeviceNameSync()}' DeviceId='${getUniqueIdSync()}'`, -) export const useInitiateQuickConnect = () => { const { api } = useJellifyContext() @@ -23,12 +16,7 @@ export const useInitiateQuickConnect = () => { mutationFn: async () => { if (isUndefined(api)) return Promise.reject(new Error('API client is not initialized')) - console.debug('Initiating Quick Connect', QUICK_CONNECT_HEADER) - return await getQuickConnectApi(api!).initiateQuickConnect({ - headers: { - Authorization: QUICK_CONNECT_HEADER, - }, - }) + return await getQuickConnectApi(api).initiateQuickConnect() }, onError: async (error: Error) => { console.error('An error occurred initiating Quick Connect', error) @@ -95,4 +83,16 @@ const useAuthenticateWithQuickConnect = () => { }) } +const useQuickConnectStatus = () => { + const { api } = useJellifyContext() + + return useMutation({ + mutationFn: async (secret: string) => { + return await getQuickConnectApi(api!).getQuickConnectState({ + secret, + }) + }, + }) +} + export default useAuthenticateWithQuickConnect diff --git a/src/api/queries/quickconnect/index.ts b/src/api/queries/quickconnect/index.ts index e7791c12..619689b3 100644 --- a/src/api/queries/quickconnect/index.ts +++ b/src/api/queries/quickconnect/index.ts @@ -10,6 +10,8 @@ const useGetQuickConnectState = (secret: string) => { queryFn: async () => { return await getQuickConnectApi(api!).getQuickConnectState({ secret }) }, + gcTime: 0, + staleTime: 0, }) } diff --git a/src/components/Login/components/quick-connect.tsx b/src/components/Login/components/quick-connect.tsx index 9585a5d8..0b233cb0 100644 --- a/src/components/Login/components/quick-connect.tsx +++ b/src/components/Login/components/quick-connect.tsx @@ -1,10 +1,12 @@ -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useLayoutEffect } from 'react' import useAuthenticateWithQuickConnect, { useInitiateQuickConnect, } from '../../../api/mutations/quickconnect' import useGetQuickConnectState from '../../../api/queries/quickconnect' -import { View, Spinner, Button, YStack } from 'tamagui' -import { Text } from '../../Global/helpers/text' +import { View, Spinner, Button, YStack, H6, H5 } from 'tamagui' +import { useFocusEffect, useNavigation } from '@react-navigation/native' +import LoginStackParamList from '@/src/screens/Login/types' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' // Handles polling, code display, error, and authentication function QuickConnectDisplay({ @@ -16,36 +18,44 @@ function QuickConnectDisplay({ code: string onExpired: () => void }) { - const { - data: stateData, - error: stateError, - isFetching: isStateFetching, - } = useGetQuickConnectState(secret) const { mutate: authenticate, isPending: isAuthenticating } = useAuthenticateWithQuickConnect() + const { + data: quickConnectData, + error: quickConnectError, + refetch: refetchQuickConnectData, + } = useGetQuickConnectState(secret) + + useEffect(() => {}, [secret, code]) + // Authenticate when ready useEffect(() => { - if (stateData?.data.Authenticated && secret) { + if (quickConnectData?.data.Authenticated && secret) { authenticate(secret) } - }, [stateData, secret, authenticate]) + }, [quickConnectData, secret, authenticate]) // Handle expired/errored code useEffect(() => { - if (stateError) { + if (quickConnectError) { onExpired() } - }, [stateError, onExpired]) + }, [quickConnectError, onExpired]) + + useEffect(() => { + const interval = setInterval(() => { + console.debug(`Checking Quick Connect State: ${JSON.stringify(quickConnectData)}`) + + if (quickConnectData?.data.Authenticated) clearInterval(interval) + refetchQuickConnectData() + }, 5000) + + return () => clearInterval(interval) + }, [secret]) return ( - {code} - {isStateFetching && } - {stateError && ( - - Code expired. Please try again. - - )} +
{code}
{isAuthenticating && }
) @@ -53,38 +63,36 @@ function QuickConnectDisplay({ // Initiates quick connect, manages secret/code state, and renders display export default function QuickConnectInitiator() { + const navigation = useNavigation>() + const { mutate: initiateQuickConnect, reset: resetInitiateQuickConnect, data: quickConnectData, - isPending: isInitiating, } = useInitiateQuickConnect() - // When QuickConnect is initiated, set secret and code - useEffect(() => { - initiateQuickConnect() - }, []) - - // Reset secret/code to retry - const handleExpired = () => { + const beginQuickConnect = useCallback(() => { resetInitiateQuickConnect() initiateQuickConnect() - } + }, [initiateQuickConnect, resetInitiateQuickConnect]) + + useEffect(() => { + initiateQuickConnect() + + return resetInitiateQuickConnect() + }) return ( - - Quick Connect - {isInitiating && } + +
Quick Connect
{quickConnectData?.data.Secret && quickConnectData?.data.Code ? ( ) : null} - {!quickConnectData?.data.Secret && !isInitiating && ( - - )} + {!quickConnectData?.data.Secret && }
) } diff --git a/src/screens/Login/server-address.tsx b/src/screens/Login/server-address.tsx index b4e5f216..92b91ee3 100644 --- a/src/screens/Login/server-address.tsx +++ b/src/screens/Login/server-address.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { isEmpty, isUndefined } from 'lodash' -import { Input, ListItem, Separator, Spinner, XStack, YGroup, YStack } from 'tamagui' +import { H3, Image, 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' @@ -62,121 +62,130 @@ export default function ServerAddress({ return ( - -

- Connect to Jellyfin -

-
+ + +

+ Connect to Jellyfin +

+
- - - {!serverAddressContainsProtocol && ( - - {useHttps ? HTTPS : HTTP} - - )} + + + {!serverAddressContainsProtocol && ( + + {useHttps ? HTTPS : HTTP} + + )} - - + { + if (!isUndefined(serverAddress)) + connectToServer({ serverAddress, useHttps }) + }} + /> + - - - - } - title='HTTPS' - subTitle='Use HTTPS to connect to Jellyfin' - disabled={serverAddressContainsProtocol} - > - setUseHttps(checked)} - label={ - serverAddressContainsHttps || useHttps - ? 'Use HTTPS' - : 'Use HTTP' + + + } - size='$2' - width={100} - /> - - - - - - - + setUseHttps(checked)} + label={ + serverAddressContainsHttps || useHttps + ? 'Use HTTPS' + : 'Use HTTP' + } + size='$2' + width={100} /> - } - title='Submit Usage and Crash Data' - subTitle='Send anonymized metrics and crash data' - > - setSendMetrics(checked)} - label='Send Metrics' - size='$2' - width={100} - /> - - - + + - {isPending ? ( - - ) : ( - - )} + + + + + } + title='Submit Usage and Crash Data' + subTitle='Send anonymized metrics and crash data' + > + setSendMetrics(checked)} + label='Send Metrics' + size='$2' + width={100} + /> + + + + +
) diff --git a/src/screens/Login/server-authentication.tsx b/src/screens/Login/server-authentication.tsx index dcbc2ead..aa3a8564 100644 --- a/src/screens/Login/server-authentication.tsx +++ b/src/screens/Login/server-authentication.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import _ from 'lodash' import { H3, H6, Spacer, Spinner, XStack, YStack } from 'tamagui' -import { H2 } from '../../components/Global/helpers/text' +import { H2, Text } from '../../components/Global/helpers/text' import Button from '../../components/Global/helpers/button' import { SafeAreaView } from 'react-native-safe-area-context' import Input from '../../components/Global/helpers/input' @@ -38,90 +38,104 @@ export default function ServerAuthentication({ return ( - -

- {`Sign in to ${server?.name ?? 'Jellyfin'}`} -

-
- {server?.version ?? 'Unknown Jellyfin version'} -
-
- - } - placeholder='Username' - value={username} - style={ - IS_MAESTRO_BUILD ? { backgroundColor: '#000', color: '#000' } : undefined - } - testID='username_input' - secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the username as screen Records - onChangeText={(value: string | undefined) => setUsername(value)} - autoCapitalize='none' - autoCorrect={false} - autoComplete='username' - textContentType='username' - importantForAutofill='yes' - returnKeyType='next' - autoFocus - /> + + +

{`Sign in to ${server?.name ?? 'Jellyfin'}`}

+
{server?.version ?? 'Unknown Jellyfin version'}
+
+ + } + placeholder='Username' + value={username} + style={ + IS_MAESTRO_BUILD + ? { backgroundColor: '#000', color: '#000' } + : undefined + } + testID='username_input' + secureTextEntry={IS_MAESTRO_BUILD} // If Maestro build, don't show the username as screen Records + onChangeText={(value: string | undefined) => setUsername(value)} + autoCapitalize='none' + autoCorrect={false} + autoComplete='username' + textContentType='username' + importantForAutofill='yes' + returnKeyType='next' + autoFocus + /> - + - } - placeholder='Password' - value={password} - testID='password_input' - style={ - IS_MAESTRO_BUILD ? { backgroundColor: '#000', color: '#000' } : undefined - } - onChangeText={(value: string | undefined) => setPassword(value)} - autoCapitalize='none' - autoCorrect={false} - secureTextEntry // Always secure text entry - autoComplete='password' - textContentType='password' - importantForAutofill='yes' - returnKeyType='go' - /> + } + placeholder='Password' + value={password} + testID='password_input' + style={ + IS_MAESTRO_BUILD + ? { backgroundColor: '#000', color: '#000' } + : undefined + } + onChangeText={(value: string | undefined) => setPassword(value)} + autoCapitalize='none' + autoCorrect={false} + secureTextEntry // Always secure text entry + autoComplete='password' + textContentType='password' + importantForAutofill='yes' + returnKeyType='go' + /> - + - - - - - + + + + + + + + + + - {isPending ? ( - - ) : ( - - )} - + {/* */}
diff --git a/src/screens/Login/server-library.tsx b/src/screens/Login/server-library.tsx index 90204989..15efbf5e 100644 --- a/src/screens/Login/server-library.tsx +++ b/src/screens/Login/server-library.tsx @@ -6,6 +6,7 @@ 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 { useInitiateQuickConnect } from '../../api/mutations/quickconnect' export default function ServerLibrary({ navigation, @@ -16,6 +17,8 @@ export default function ServerLibrary({ const rootNavigation = useNavigation>() + const initiateQuickConnect = useInitiateQuickConnect() + const handleLibrarySelected = ( libraryId: string, selectedLibrary: BaseItemDto, @@ -32,9 +35,8 @@ export default function ServerLibrary({ } const handleCancel = () => { - navigation.navigate('ServerAuthentication', undefined, { - pop: true, - }) + initiateQuickConnect.reset() + navigation.popTo('ServerAuthentication', undefined) } return (