diff --git a/src/constants/locales.ts b/src/constants/locales.ts index 716c41d9..5b277e70 100644 --- a/src/constants/locales.ts +++ b/src/constants/locales.ts @@ -9,6 +9,6 @@ export const LOCALES = [ { code: 'es', key: 'spanish' }, { code: 'hr', key: 'croatian' }, { code: 'zh-CN', key: 'chinese' }, - { code: 'hi', key: 'hindi' } + { code: 'hi', key: 'hindi' }, { code: 'sr', key: 'serbian' } ]; \ No newline at end of file diff --git a/src/screens/settings/PlaybackSettingsScreen.tsx b/src/screens/settings/PlaybackSettingsScreen.tsx index fb549e1e..c3559e3c 100644 --- a/src/screens/settings/PlaybackSettingsScreen.tsx +++ b/src/screens/settings/PlaybackSettingsScreen.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; -import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, TextInput } from 'react-native'; +import { View, StyleSheet, ScrollView, StatusBar, Platform, Text, TouchableOpacity, Dimensions, TextInput, ActivityIndicator } from 'react-native'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -14,6 +14,7 @@ import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@g import { useTranslation } from 'react-i18next'; import { SvgXml } from 'react-native-svg'; import { toastService } from '../../services/toastService'; +import { introService } from '../../services/introService'; const { width } = Dimensions.get('window'); @@ -79,14 +80,40 @@ export const PlaybackSettingsContent: React.FC = ( const [introDbLogoXml, setIntroDbLogoXml] = useState(null); const [apiKeyInput, setApiKeyInput] = useState(settings?.introDbApiKey || ''); + const [isVerifyingKey, setIsVerifyingKey] = useState(false); + + const isMounted = useRef(true); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); useEffect(() => { setApiKeyInput(settings?.introDbApiKey || ''); }, [settings?.introDbApiKey]); - const handleApiKeySubmit = () => { - updateSetting('introDbApiKey', apiKeyInput); - toastService.success(t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' })); + const handleApiKeySubmit = async () => { + if (!apiKeyInput.trim()) { + updateSetting('introDbApiKey', ''); + toastService.success(t('settings.items.api_key_cleared', { defaultValue: 'API Key Cleared' })); + return; + } + + setIsVerifyingKey(true); + const isValid = await introService.verifyApiKey(apiKeyInput); + + if (!isMounted.current) return; + setIsVerifyingKey(false); + + if (isValid) { + updateSetting('introDbApiKey', apiKeyInput); + toastService.success(t('settings.items.api_key_saved', { defaultValue: 'API Key Saved' })); + } else { + toastService.error(t('settings.items.api_key_invalid', { defaultValue: 'Invalid API Key' })); + } }; useEffect(() => { @@ -271,8 +298,13 @@ export const PlaybackSettingsContent: React.FC = ( - + {isVerifyingKey ? ( + + ) : ( + + )} diff --git a/src/services/introService.ts b/src/services/introService.ts index 99772144..de9a1c78 100644 --- a/src/services/introService.ts +++ b/src/services/introService.ts @@ -187,6 +187,40 @@ async function fetchFromIntroDb(imdbId: string, season: number, episode: number) } } +/** + * Verifies an IntroDB API key + */ +export async function verifyApiKey(apiKey: string): Promise { + try { + if (!apiKey) return false; + + const response = await axios.post(`${INTRODB_API_URL}/submit`, {}, { + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + timeout: 5000, + validateStatus: (status) => true // Handle status codes manually + }); + + // 400 means Auth passed but payload was empty/invalid -> Key is Valid + if (response.status === 400) return true; + + // 200/201 would also mean valid (though unexpected with empty body) + if (response.status === 200 || response.status === 201) return true; + + // Explicitly handle auth failures + if (response.status === 401 || response.status === 403) return false; + + // Log warning for unexpected states (500, 429, etc.) but fail safe + logger.warn(`[IntroService] Verification received unexpected status: ${response.status}`); + return false; + } catch (error: any) { + logger.log('[IntroService] API Key verification failed:', error.message); + return false; + } +} + /** * Submits an intro timestamp to IntroDB */ @@ -305,7 +339,8 @@ export async function getIntroTimestamps( export const introService = { getIntroTimestamps, getSkipTimes, - submitIntro + submitIntro, + verifyApiKey }; export default introService;