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 (