Improved Localization in AI Screens

This commit is contained in:
albyalex96 2026-03-05 19:29:09 +01:00
parent 07542d2838
commit 60ca025ece
4 changed files with 2172 additions and 1739 deletions

View file

@ -14,6 +14,7 @@
"try_again": "Try Again",
"go_back": "Go Back",
"settings": "Settings",
"remove":"Remove",
"close": "Close",
"enable": "Enable",
"disable": "Disable",
@ -926,7 +927,11 @@
"confirm_remove_title": "Remove API Key",
"confirm_remove_msg": "Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.",
"success_removed": "API key removed successfully",
"error_remove": "Failed to remove API key"
"error_remove": "Failed to remove API key",
"model":"Model",
"using": "Using",
"free_routing":"(free automatic routing)",
"paid_routing":"Use a custom OpenRouter model ID (useful for paid plans)."
},
"catalog_settings": {
"title": "Catalogs",
@ -1523,5 +1528,8 @@
"user_id": "User ID",
"display_name": "Display Name",
"display_name_placeholder": "Add a display name"
},
"ai_chat_screen":{
"loading":"Loading AI context..."
}
}

View file

@ -15,6 +15,7 @@
"go_back": "Torna indietro",
"settings": "Impostazioni",
"close": "Chiudi",
"remove":"Rimuovi",
"enable": "Abilita",
"disable": "Disabilita",
"show_more": "Mostra altro",
@ -946,7 +947,11 @@
"confirm_remove_title": "Rimuovi Chiave API",
"confirm_remove_msg": "Sei sicuro di voler rimuovere la tua chiave API OpenRouter? Questo disabiliterà le funzioni di chat IA.",
"success_removed": "Chiave API rimossa con successo",
"error_remove": "Impossibile rimuovere la chiave API"
"error_remove": "Impossibile rimuovere la chiave API",
"model":"Modello",
"using":"Usando",
"free_routing":"(Routing automatico gratuito)",
"paid_routing":"Usa un ID di modello Openrouter (utile per piani a pagamento)."
},
"catalog_settings": {
"title": "Cataloghi",
@ -1543,5 +1548,8 @@
"user_id": "ID Utente",
"display_name": "Nickname",
"display_name_placeholder": "Aggiungi un nickname"
},
"ai_chat_screen": {
"loading": "Caricamento contesto AI in corso..."
}
}

View file

@ -1,4 +1,10 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from 'react';
import {
View,
Text,
@ -14,7 +20,12 @@ import {
Keyboard,
} from 'react-native';
import CustomAlert from '../components/CustomAlert';
import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native';
import {
useRoute,
useNavigation,
RouteProp,
useFocusEffect,
} from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import FastImage from '@d11/react-native-fast-image';
@ -39,14 +50,28 @@ if (Platform.OS === 'ios') {
// Dynamically require so app still runs if the package isn't installed yet
const glass = require('expo-glass-effect');
GlassViewComp = glass.GlassView;
liquidGlassAvailable = typeof glass.isLiquidGlassAvailable === 'function' ? glass.isLiquidGlassAvailable() : false;
liquidGlassAvailable =
typeof glass.isLiquidGlassAvailable === 'function'
? glass.isLiquidGlassAvailable()
: false;
} catch {
GlassViewComp = null;
liquidGlassAvailable = false;
}
}
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, createSeriesContext, generateConversationStarters } from '../services/aiService';
import {
useSafeAreaInsets,
SafeAreaView,
} from 'react-native-safe-area-context';
import {
aiService,
ChatMessage,
ContentContext,
createMovieContext,
createEpisodeContext,
createSeriesContext,
generateConversationStarters,
} from '../services/aiService';
import { tmdbService } from '../services/tmdbService';
import Markdown from 'react-native-markdown-display';
import Animated, {
@ -59,8 +84,9 @@ import Animated, {
withDelay,
interpolate,
Extrapolate,
runOnJS
runOnJS,
} from 'react-native-reanimated';
import { useTranslation } from 'react-i18next';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
@ -84,7 +110,10 @@ interface ChatBubbleProps {
}
// Animated typing dot component
const TypingDot: React.FC<{ delay: number; color: string }> = ({ delay, color }) => {
const TypingDot: React.FC<{ delay: number; color: string }> = ({
delay,
color,
}) => {
const opacity = useSharedValue(0.3);
const scale = useSharedValue(1);
@ -94,22 +123,22 @@ const TypingDot: React.FC<{ delay: number; color: string }> = ({ delay, color })
withRepeat(
withSequence(
withTiming(1, { duration: 400 }),
withTiming(0.3, { duration: 400 })
withTiming(0.3, { duration: 400 }),
),
-1,
false
)
false,
),
);
scale.value = withDelay(
delay,
withRepeat(
withSequence(
withTiming(1.2, { duration: 400 }),
withTiming(1, { duration: 400 })
withTiming(1, { duration: 400 }),
),
-1,
false
)
false,
),
);
}, []);
@ -119,11 +148,14 @@ const TypingDot: React.FC<{ delay: number; color: string }> = ({ delay, color })
}));
return (
<Animated.View style={[styles.typingDot, { backgroundColor: color }, animatedStyle]} />
<Animated.View
style={[styles.typingDot, { backgroundColor: color }, animatedStyle]}
/>
);
};
const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) => {
const ChatBubble: React.FC<ChatBubbleProps> = React.memo(
({ message, isLast }) => {
const { currentTheme } = useTheme();
const isUser = message.role === 'user';
@ -141,31 +173,36 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
bubbleAnimation.value,
[0, 1],
[16, 0],
Extrapolate.CLAMP
)
Extrapolate.CLAMP,
),
},
{
scale: interpolate(
bubbleAnimation.value,
[0, 1],
[0.95, 1],
Extrapolate.CLAMP
)
}
]
Extrapolate.CLAMP,
),
},
],
}));
return (
<Animated.View style={[
<Animated.View
style={[
styles.messageContainer,
isUser ? styles.userMessageContainer : styles.assistantMessageContainer,
isLast && styles.lastMessageContainer,
animatedStyle
]}>
animatedStyle,
]}
>
{!isUser && (
<View style={styles.avatarWrapper}>
<LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}99`]}
colors={[
currentTheme.colors.primary,
`${currentTheme.colors.primary}99`,
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.avatarContainer}
@ -175,9 +212,11 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
</View>
)}
<View style={[
<View
style={[
styles.messageBubble,
isUser ? [
isUser
? [
styles.userBubble,
{
backgroundColor: currentTheme.colors.primary,
@ -186,20 +225,37 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 6,
}
] : [
styles.assistantBubble,
{ backgroundColor: 'transparent' }
},
]
]}>
: [styles.assistantBubble, { backgroundColor: 'transparent' }],
]}
>
{!isUser && (
<View style={styles.assistantBlurBackdrop} pointerEvents="none">
{Platform.OS === 'android' && AndroidBlurView
? <AndroidBlurView blurAmount={18} blurRadius={10} style={StyleSheet.absoluteFill} />
: Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable
? <GlassViewComp style={StyleSheet.absoluteFill} glassEffectStyle="regular" />
: <ExpoBlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.45)' }]} />
{Platform.OS === 'android' && AndroidBlurView ? (
<AndroidBlurView
blurAmount={18}
blurRadius={10}
style={StyleSheet.absoluteFill}
/>
) : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable ? (
<GlassViewComp
style={StyleSheet.absoluteFill}
glassEffectStyle="regular"
/>
) : (
<ExpoBlurView
intensity={80}
tint="dark"
style={StyleSheet.absoluteFill}
/>
)}
<View
style={[
StyleSheet.absoluteFill,
{ backgroundColor: 'rgba(0,0,0,0.45)' },
]}
/>
</View>
)}
{isUser ? (
@ -220,7 +276,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
paragraph: {
marginBottom: 12,
marginTop: 0,
color: currentTheme.colors.highEmphasis
color: currentTheme.colors.highEmphasis,
},
heading1: {
fontSize: 22,
@ -247,7 +303,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
},
link: {
color: currentTheme.colors.primary,
textDecorationLine: 'underline'
textDecorationLine: 'underline',
},
code_inline: {
backgroundColor: 'rgba(255,255,255,0.08)',
@ -282,23 +338,23 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
},
bullet_list: {
marginBottom: 10,
marginTop: 0
marginTop: 0,
},
ordered_list: {
marginBottom: 10,
marginTop: 0
marginTop: 0,
},
list_item: {
marginBottom: 6,
color: currentTheme.colors.highEmphasis
color: currentTheme.colors.highEmphasis,
},
strong: {
fontWeight: '700',
color: currentTheme.colors.highEmphasis
color: currentTheme.colors.highEmphasis,
},
em: {
fontStyle: 'italic',
color: currentTheme.colors.mediumEmphasis
color: currentTheme.colors.mediumEmphasis,
},
blockquote: {
backgroundColor: 'rgba(255,255,255,0.04)',
@ -337,29 +393,43 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
{message.content}
</Markdown>
)}
<Text style={[
<Text
style={[
styles.messageTime,
{ color: isUser ? 'rgba(255,255,255,0.65)' : currentTheme.colors.disabled }
]}>
{
color: isUser ? 'rgba(255,255,255,0.65)' : currentTheme.colors.disabled,
},
]}
>
{new Date(message.timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
minute: '2-digit',
})}
</Text>
</View>
{isUser && (
<View style={[styles.userAvatarContainer, {
<View
style={[
styles.userAvatarContainer,
{
backgroundColor: 'rgba(255,255,255,0.08)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.12)',
}]}>
<MaterialIcons name="person" size={14} color={currentTheme.colors.primary} />
},
]}
>
<MaterialIcons
name="person"
size={14}
color={currentTheme.colors.primary}
/>
</View>
)}
</Animated.View>
);
}, (prev, next) => {
},
(prev, next) => {
return (
prev.isLast === next.isLast &&
prev.message.id === next.message.id &&
@ -367,7 +437,8 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
prev.message.content === next.message.content &&
prev.message.timestamp === next.message.timestamp
);
});
},
);
interface SuggestionChipProps {
text: string;
@ -375,22 +446,32 @@ interface SuggestionChipProps {
index: number;
}
const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPress, index }) => {
const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(
({ text, onPress, index }) => {
const { currentTheme } = useTheme();
const animValue = useSharedValue(0);
useEffect(() => {
animValue.value = withDelay(
index * 80,
withSpring(1, { damping: 18, stiffness: 120 })
withSpring(1, { damping: 18, stiffness: 120 }),
);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: animValue.value,
transform: [
{ translateY: interpolate(animValue.value, [0, 1], [12, 0], Extrapolate.CLAMP) },
{ scale: interpolate(animValue.value, [0, 1], [0.95, 1], Extrapolate.CLAMP) },
{
translateY: interpolate(
animValue.value,
[0, 1],
[12, 0],
Extrapolate.CLAMP,
),
},
{
scale: interpolate(animValue.value, [0, 1], [0.95, 1], Extrapolate.CLAMP),
},
],
}));
@ -403,7 +484,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
backgroundColor: 'rgba(255,255,255,0.04)',
borderWidth: 1,
borderColor: `${currentTheme.colors.primary}40`,
}
},
]}
onPress={onPress}
activeOpacity={0.7}
@ -414,7 +495,12 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
color={currentTheme.colors.primary}
style={styles.suggestionIcon}
/>
<Text style={[styles.suggestionText, { color: currentTheme.colors.highEmphasis }]}>
<Text
style={[
styles.suggestionText,
{ color: currentTheme.colors.highEmphasis },
]}
>
{text}
</Text>
<MaterialIcons
@ -425,31 +511,40 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
</TouchableOpacity>
</Animated.View>
);
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress && prev.index === next.index);
},
(prev, next) =>
prev.text === next.text &&
prev.onPress === next.onPress &&
prev.index === next.index,
);
const AIChatScreen: React.FC = () => {
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
]);
const {t } = useTranslation();
const [alertActions, setAlertActions] = useState<
Array<{ label: string; onPress: () => void; style?: object }>
>([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
const openAlert = (
title: string,
message: string,
actions?: Array<{ label: string; onPress?: () => void; style?: object }>
actions?: Array<{ label: string; onPress?: () => void; style?: object }>,
) => {
setAlertTitle(title);
setAlertMessage(message);
if (actions && actions.length > 0) {
setAlertActions(
actions.map(a => ({
actions.map((a) => ({
label: a.label,
style: a.style,
onPress: () => { a.onPress?.(); setAlertVisible(false); },
}))
onPress: () => {
a.onPress?.();
setAlertVisible(false);
},
})),
);
} else {
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
@ -461,7 +556,14 @@ const AIChatScreen: React.FC = () => {
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const { contentId, contentType, episodeId, seasonNumber, episodeNumber, title } = route.params;
const {
contentId,
contentType,
episodeId,
seasonNumber,
episodeNumber,
title,
} = route.params;
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputText, setInputText] = useState('');
@ -480,7 +582,7 @@ const AIChatScreen: React.FC = () => {
setBackdropUrl(null);
}
};
}, [])
}, []),
);
const scrollViewRef = useRef<ScrollView>(null);
@ -513,10 +615,12 @@ const AIChatScreen: React.FC = () => {
}
};
const showSub = Platform.OS === 'ios'
const showSub =
Platform.OS === 'ios'
? Keyboard.addListener('keyboardWillShow', onShow)
: Keyboard.addListener('keyboardDidShow', onShow);
const hideSub = Platform.OS === 'ios'
const hideSub =
Platform.OS === 'ios'
? Keyboard.addListener('keyboardWillHide', onHide)
: Keyboard.addListener('keyboardDidHide', onHide);
@ -582,7 +686,7 @@ const AIChatScreen: React.FC = () => {
const [showData, allEpisodes] = await Promise.all([
tmdbService.getTVShowDetails(tmdbNumericId),
tmdbService.getAllEpisodes(tmdbNumericId)
tmdbService.getAllEpisodes(tmdbNumericId),
]);
if (!showData) throw new Error('Unable to load TV show details');
@ -600,18 +704,21 @@ const AIChatScreen: React.FC = () => {
openAlert('Error', 'Failed to load content details for AI chat');
} finally {
setIsLoadingContext(false);
{/* CustomAlert at root */ }
{
/* CustomAlert at root */
}
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
/>;
}
};
const sendMessage = useCallback(async (messageText: string) => {
const sendMessage = useCallback(
async (messageText: string) => {
if (!messageText.trim() || !context || isLoading) return;
const userMessage: ChatMessage = {
@ -621,7 +728,7 @@ const AIChatScreen: React.FC = () => {
timestamp: Date.now(),
};
setMessages(prev => [...prev, userMessage]);
setMessages((prev) => [...prev, userMessage]);
setInputText('');
setIsLoading(true);
setSuggestions([]); // Hide suggestions after first message
@ -631,7 +738,9 @@ const AIChatScreen: React.FC = () => {
let requestContext = context;
if ('episodesBySeason' in (context as any)) {
// Series-wide context; optionally detect SxE patterns to focus answer, but keep series context
const sxe = messageText.match(/s(\d+)e(\d+)/i) || messageText.match(/season\s+(\d+)[^\d]+episode\s+(\d+)/i);
const sxe =
messageText.match(/s(\d+)e(\d+)/i) ||
messageText.match(/season\s+(\d+)[^\d]+episode\s+(\d+)/i);
if (sxe) {
// We will append a brief hint to the user question to scope, but still pass series context
messageText = `${messageText} (about Season ${sxe[1]}, Episode ${sxe[2]})`;
@ -639,10 +748,19 @@ const AIChatScreen: React.FC = () => {
} else if ('showTitle' in (context as any)) {
const sxe = messageText.match(/s(\d+)e(\d+)/i);
const words = messageText.match(/season\s+(\d+)[^\d]+episode\s+(\d+)/i);
const seasonOnly = messageText.match(/s(\d+)(?!e)/i) || messageText.match(/season\s+(\d+)/i);
const seasonOnly =
messageText.match(/s(\d+)(?!e)/i) || messageText.match(/season\s+(\d+)/i);
let season = sxe ? parseInt(sxe[1], 10) : (words ? parseInt(words[1], 10) : undefined);
let episode = sxe ? parseInt(sxe[2], 10) : (words ? parseInt(words[2], 10) : undefined);
let season = sxe
? parseInt(sxe[1], 10)
: words
? parseInt(words[1], 10)
: undefined;
let episode = sxe
? parseInt(sxe[2], 10)
: words
? parseInt(words[2], 10)
: undefined;
// If only season mentioned (like "s2" or "season 2"), default to episode 1
if (!season && seasonOnly) {
@ -659,16 +777,22 @@ const AIChatScreen: React.FC = () => {
} else {
tmdbNumericId = await tmdbService.findTMDBIdByIMDB(contentId);
if (!tmdbNumericId && episodeId) {
tmdbNumericId = await tmdbService.extractTMDBIdFromStremioId(episodeId);
tmdbNumericId =
await tmdbService.extractTMDBIdFromStremioId(episodeId);
}
}
if (tmdbNumericId) {
const [showData, episodeData] = await Promise.all([
tmdbService.getTVShowDetails(tmdbNumericId),
tmdbService.getEpisodeDetails(tmdbNumericId, season, episode)
tmdbService.getEpisodeDetails(tmdbNumericId, season, episode),
]);
if (showData && episodeData) {
requestContext = createEpisodeContext(episodeData, showData, season, episode);
requestContext = createEpisodeContext(
episodeData,
showData,
season,
episode,
);
}
}
} catch {}
@ -678,7 +802,7 @@ const AIChatScreen: React.FC = () => {
const response = await aiService.sendMessage(
messageText.trim(),
requestContext,
messages
messages,
);
const assistantMessage: ChatMessage = {
@ -688,22 +812,33 @@ const AIChatScreen: React.FC = () => {
timestamp: Date.now(),
};
setMessages(prev => [...prev, assistantMessage]);
setMessages((prev) => [...prev, assistantMessage]);
} catch (error) {
if (__DEV__) console.error('Error sending message:', error);
let errorMessage = 'Sorry, I encountered an error. Please try again.';
if (error instanceof Error) {
if (error.message.includes('not configured')) {
errorMessage = 'Please configure your OpenRouter API key in Settings > AI Assistant.';
} else if (/401|unauthorized|invalid api key|authentication/i.test(error.message)) {
errorMessage = 'OpenRouter rejected your API key. Please verify the key in Settings > AI Assistant.';
errorMessage =
'Please configure your OpenRouter API key in Settings > AI Assistant.';
} else if (
/401|unauthorized|invalid api key|authentication/i.test(error.message)
) {
errorMessage =
'OpenRouter rejected your API key. Please verify the key in Settings > AI Assistant.';
} else if (/insufficient|credit|quota|429/i.test(error.message)) {
errorMessage = 'OpenRouter quota/credits were rejected for this request. Please check your OpenRouter usage and limits.';
} else if (/model|provider|endpoint|unsupported|unavailable|not found/i.test(error.message)) {
errorMessage = 'The selected OpenRouter model is unavailable. Retry with `openrouter/free` or choose another custom model in Settings > AI Assistant.';
errorMessage =
'OpenRouter quota/credits were rejected for this request. Please check your OpenRouter usage and limits.';
} else if (
/model|provider|endpoint|unsupported|unavailable|not found/i.test(
error.message,
)
) {
errorMessage =
'The selected OpenRouter model is unavailable. Retry with `openrouter/free` or choose another custom model in Settings > AI Assistant.';
} else if (error.message.includes('API request failed')) {
errorMessage = 'Failed to connect to AI service. Please check your internet connection, API key, and OpenRouter model availability.';
errorMessage =
'Failed to connect to AI service. Please check your internet connection, API key, and OpenRouter model availability.';
}
}
@ -714,19 +849,24 @@ const AIChatScreen: React.FC = () => {
timestamp: Date.now(),
};
setMessages(prev => [...prev, errorResponse]);
setMessages((prev) => [...prev, errorResponse]);
} finally {
setIsLoading(false);
}
}, [context, messages, isLoading]);
},
[context, messages, isLoading],
);
const handleSendPress = useCallback(() => {
sendMessage(inputText);
}, [inputText, sendMessage]);
const handleSuggestionPress = useCallback((suggestion: string) => {
const handleSuggestionPress = useCallback(
(suggestion: string) => {
sendMessage(suggestion);
}, [sendMessage]);
},
[sendMessage],
);
const scrollToBottom = useCallback(() => {
setTimeout(() => {
@ -750,7 +890,9 @@ const AIChatScreen: React.FC = () => {
// For episode contexts, now also only show show title to avoid episode in title per requirement
return (context as any).showTitle;
}
return ('title' in (context as any) && (context as any).title) ? (context as any).title : title;
return 'title' in (context as any) && (context as any).title
? (context as any).title
: title;
};
const headerAnimatedStyle = useAnimatedStyle(() => ({
@ -763,11 +905,18 @@ const AIChatScreen: React.FC = () => {
if (isLoadingContext) {
return (
<View style={[styles.loadingContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
<View
style={[
styles.loadingContainer,
{ backgroundColor: currentTheme.colors.darkBackground },
]}
>
<StatusBar barStyle="light-content" />
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}>
Loading AI context...
<Text
style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}
>
{t('ai_chat_screen.loading')}
</Text>
</View>
);
@ -775,7 +924,13 @@ const AIChatScreen: React.FC = () => {
return (
<Animated.View style={{ flex: 1, opacity: modalOpacity }}>
<SafeAreaView edges={['top', 'bottom']} style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<SafeAreaView
edges={['top', 'bottom']}
style={[
styles.container,
{ backgroundColor: currentTheme.colors.darkBackground },
]}
>
{backdropUrl && (
<View style={StyleSheet.absoluteFill} pointerEvents="none">
<FastImage
@ -783,52 +938,96 @@ const AIChatScreen: React.FC = () => {
style={StyleSheet.absoluteFill}
resizeMode={FastImage.resizeMode.cover}
/>
{Platform.OS === 'android' && AndroidBlurView
? <AndroidBlurView blurAmount={12} blurRadius={6} style={StyleSheet.absoluteFill} />
: Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable
? <GlassViewComp style={StyleSheet.absoluteFill} glassEffectStyle="regular" />
: <ExpoBlurView intensity={60} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: Platform.OS === 'android' ? 'rgba(0,0,0,0.28)' : 'rgba(0,0,0,0.45)' }]} />
{Platform.OS === 'android' && AndroidBlurView ? (
<AndroidBlurView
blurAmount={12}
blurRadius={6}
style={StyleSheet.absoluteFill}
/>
) : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable ? (
<GlassViewComp
style={StyleSheet.absoluteFill}
glassEffectStyle="regular"
/>
) : (
<ExpoBlurView
intensity={60}
tint="dark"
style={StyleSheet.absoluteFill}
/>
)}
<View
style={[
StyleSheet.absoluteFill,
{
backgroundColor:
Platform.OS === 'android' ? 'rgba(0,0,0,0.28)' : 'rgba(0,0,0,0.45)',
},
]}
/>
</View>
)}
<StatusBar barStyle="light-content" />
{/* Header */}
<Animated.View style={[
<Animated.View
style={[
styles.header,
{
backgroundColor: 'transparent',
paddingTop: Platform.OS === 'ios' ? insets.top : insets.top
paddingTop: Platform.OS === 'ios' ? insets.top : insets.top,
},
headerAnimatedStyle
]}>
headerAnimatedStyle,
]}
>
<View style={styles.headerContent}>
<TouchableOpacity
onPress={() => {
if (Platform.OS === 'android') {
modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => {
modalOpacity.value = withSpring(
0,
{ damping: 18, stiffness: 160 },
(finished) => {
if (finished) runOnJS(navigation.goBack)();
});
},
);
} else {
navigation.goBack();
}
}}
style={styles.backButton}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
/>
</TouchableOpacity>
<View style={styles.headerInfo}>
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
<Text
style={[
styles.headerTitle,
{ color: currentTheme.colors.highEmphasis },
]}
>
AI Chat
</Text>
<Text style={[styles.headerSubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
<Text
style={[
styles.headerSubtitle,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{getDisplayTitle()}
</Text>
</View>
<LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}CC`]}
colors={[
currentTheme.colors.primary,
`${currentTheme.colors.primary}CC`,
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.aiIndicator}
@ -849,7 +1048,7 @@ const AIChatScreen: React.FC = () => {
style={styles.messagesContainer}
contentContainerStyle={[
styles.messagesContent,
{ paddingBottom: isKeyboardVisible ? 20 : (56 + (isLoading ? 20 : 0)) }
{ paddingBottom: isKeyboardVisible ? 20 : 56 + (isLoading ? 20 : 0) },
]}
showsVerticalScrollIndicator={false}
removeClippedSubviews
@ -859,25 +1058,49 @@ const AIChatScreen: React.FC = () => {
{messages.length === 0 && suggestions.length > 0 && (
<View style={styles.welcomeContainer}>
<LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}99`]}
colors={[
currentTheme.colors.primary,
`${currentTheme.colors.primary}99`,
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.welcomeIcon}
>
<MaterialIcons name="auto-awesome" size={34} color="white" />
</LinearGradient>
<Text style={[styles.welcomeTitle, { color: currentTheme.colors.highEmphasis }]}>
<Text
style={[
styles.welcomeTitle,
{ color: currentTheme.colors.highEmphasis },
]}
>
Ask me anything about
</Text>
<Text style={[styles.welcomeSubtitle, { color: currentTheme.colors.primary }]}>
<Text
style={[
styles.welcomeSubtitle,
{ color: currentTheme.colors.primary },
]}
>
{getDisplayTitle()}
</Text>
<Text style={[styles.welcomeDescription, { color: currentTheme.colors.mediumEmphasis }]}>
I have detailed knowledge about this content and can answer questions about plot, characters, themes, and more.
<Text
style={[
styles.welcomeDescription,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
I have detailed knowledge about this content and can answer questions
about plot, characters, themes, and more.
</Text>
<View style={styles.suggestionsContainer}>
<Text style={[styles.suggestionsTitle, { color: currentTheme.colors.mediumEmphasis }]}>
<Text
style={[
styles.suggestionsTitle,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
Try asking:
</Text>
<View style={styles.suggestionsGrid}>
@ -904,7 +1127,12 @@ const AIChatScreen: React.FC = () => {
{isLoading && (
<View style={styles.typingIndicator}>
<View style={[styles.typingBubble, { backgroundColor: 'rgba(255,255,255,0.06)' }]}>
<View
style={[
styles.typingBubble,
{ backgroundColor: 'rgba(255,255,255,0.06)' },
]}
>
<View style={styles.typingDots}>
<TypingDot delay={0} color={currentTheme.colors.primary} />
<TypingDot delay={150} color={currentTheme.colors.primary} />
@ -916,30 +1144,55 @@ const AIChatScreen: React.FC = () => {
</ScrollView>
{/* Input Container */}
<SafeAreaView edges={['bottom']} style={{ backgroundColor: 'transparent' }}>
<Animated.View style={[
<SafeAreaView
edges={['bottom']}
style={{ backgroundColor: 'transparent' }}
>
<Animated.View
style={[
styles.inputContainer,
{
backgroundColor: 'transparent',
paddingBottom: 12
paddingBottom: 12,
},
inputAnimatedStyle
]}>
inputAnimatedStyle,
]}
>
<View style={[styles.inputWrapper, { backgroundColor: 'transparent' }]}>
<View style={styles.inputBlurBackdrop} pointerEvents="none">
{Platform.OS === 'android' && AndroidBlurView
? <AndroidBlurView blurAmount={10} blurRadius={4} style={StyleSheet.absoluteFill} />
: Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable
? <GlassViewComp style={StyleSheet.absoluteFill} glassEffectStyle="regular" />
: <ExpoBlurView intensity={50} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: Platform.OS === 'android' ? 'rgba(0,0,0,0.15)' : 'rgba(0,0,0,0.25)' }]} />
{Platform.OS === 'android' && AndroidBlurView ? (
<AndroidBlurView
blurAmount={10}
blurRadius={4}
style={StyleSheet.absoluteFill}
/>
) : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable ? (
<GlassViewComp
style={StyleSheet.absoluteFill}
glassEffectStyle="regular"
/>
) : (
<ExpoBlurView
intensity={50}
tint="dark"
style={StyleSheet.absoluteFill}
/>
)}
<View
style={[
StyleSheet.absoluteFill,
{
backgroundColor:
Platform.OS === 'android'
? 'rgba(0,0,0,0.15)'
: 'rgba(0,0,0,0.25)',
},
]}
/>
</View>
<TextInput
ref={inputRef}
style={[
styles.textInput,
{ color: currentTheme.colors.highEmphasis }
]}
style={[styles.textInput, { color: currentTheme.colors.highEmphasis }]}
value={inputText}
onChangeText={setInputText}
placeholder="Ask about this content..."
@ -959,7 +1212,10 @@ const AIChatScreen: React.FC = () => {
>
{inputText.trim() ? (
<LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}DD`]}
colors={[
currentTheme.colors.primary,
`${currentTheme.colors.primary}DD`,
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.sendButton}
@ -967,7 +1223,12 @@ const AIChatScreen: React.FC = () => {
<MaterialIcons name="arrow-upward" size={22} color="white" />
</LinearGradient>
) : (
<View style={[styles.sendButton, { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
<View
style={[
styles.sendButton,
{ backgroundColor: 'rgba(255,255,255,0.08)' },
]}
>
<MaterialIcons
name="arrow-upward"
size={22}

View file

@ -33,24 +33,26 @@ const AISettingsScreen: React.FC = () => {
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
]);
const [alertActions, setAlertActions] = useState<
Array<{ label: string; onPress: () => void; style?: object }>
>([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
const openAlert = (
title: string,
message: string,
actions?: Array<{ label: string; onPress?: () => void; style?: object }>
actions?: Array<{ label: string; onPress?: () => void; style?: object }>,
) => {
setAlertTitle(title);
setAlertMessage(message);
if (actions && actions.length > 0) {
setAlertActions(
actions.map(a => ({
actions.map((a) => ({
label: a.label,
style: a.style,
onPress: () => { a.onPress?.(); },
}))
onPress: () => {
a.onPress?.();
},
})),
);
} else {
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
@ -141,7 +143,7 @@ const AISettingsScreen: React.FC = () => {
[
{ label: t('common.cancel'), onPress: () => {} },
{
label: 'Remove',
label: t('common.remove'),
onPress: async () => {
try {
await mmkvStorage.removeItem('openrouter_api_key');
@ -151,9 +153,9 @@ const AISettingsScreen: React.FC = () => {
} catch (error) {
openAlert(t('common.error'), t('ai_settings.error_remove'));
}
}
}
]
},
},
],
);
};
@ -162,7 +164,12 @@ const AISettingsScreen: React.FC = () => {
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<SafeAreaView
style={[
styles.container,
{ backgroundColor: currentTheme.colors.darkBackground },
]}
>
<StatusBar barStyle="light-content" />
{/* Header */}
@ -196,43 +203,91 @@ const AISettingsScreen: React.FC = () => {
showsVerticalScrollIndicator={false}
>
{/* Info Card */}
<View style={[styles.infoCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View
style={[
styles.infoCard,
{ backgroundColor: currentTheme.colors.elevation1 },
]}
>
<View style={styles.infoHeader}>
<MaterialIcons
name="smart-toy"
size={24}
color={currentTheme.colors.primary}
/>
<Text style={[styles.infoTitle, { color: currentTheme.colors.highEmphasis }]}>
<Text
style={[styles.infoTitle, { color: currentTheme.colors.highEmphasis }]}
>
{t('ai_settings.info_title')}
</Text>
</View>
<Text style={[styles.infoDescription, { color: currentTheme.colors.mediumEmphasis }]}>
<Text
style={[
styles.infoDescription,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{t('ai_settings.info_desc')}
</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
<MaterialIcons
name="check-circle"
size={16}
color={currentTheme.colors.primary}
/>
<Text
style={[
styles.featureText,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{t('ai_settings.feature_1')}
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
<MaterialIcons
name="check-circle"
size={16}
color={currentTheme.colors.primary}
/>
<Text
style={[
styles.featureText,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{t('ai_settings.feature_2')}
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
<MaterialIcons
name="check-circle"
size={16}
color={currentTheme.colors.primary}
/>
<Text
style={[
styles.featureText,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{t('ai_settings.feature_3')}
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
<MaterialIcons
name="check-circle"
size={16}
color={currentTheme.colors.primary}
/>
<Text
style={[
styles.featureText,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{t('ai_settings.feature_4')}
</Text>
</View>
@ -240,8 +295,12 @@ const AISettingsScreen: React.FC = () => {
</View>
{/* API Key Configuration */}
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
<View
style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }]}
>
<Text
style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}
>
{t('ai_settings.api_key_section')}
</Text>
@ -249,7 +308,12 @@ const AISettingsScreen: React.FC = () => {
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>
{t('ai_settings.api_key_label')}
</Text>
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
<Text
style={[
styles.description,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{t('ai_settings.api_key_desc')}
</Text>
@ -259,8 +323,8 @@ const AISettingsScreen: React.FC = () => {
{
backgroundColor: currentTheme.colors.elevation2,
color: currentTheme.colors.highEmphasis,
borderColor: currentTheme.colors.elevation2
}
borderColor: currentTheme.colors.elevation2,
},
]}
value={apiKey}
onChangeText={setApiKey}
@ -273,21 +337,35 @@ const AISettingsScreen: React.FC = () => {
<View style={styles.modelSection}>
<View style={styles.modelHeader}>
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>
Model
<Text
style={[styles.label, { color: currentTheme.colors.highEmphasis }]}
>
{t('ai_settings.model')}
</Text>
<Switch
value={useDefaultModel}
onValueChange={setUseDefaultModel}
trackColor={{ false: currentTheme.colors.elevation2, true: currentTheme.colors.primary }}
thumbColor={useDefaultModel ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis}
trackColor={{
false: currentTheme.colors.elevation2,
true: currentTheme.colors.primary,
}}
thumbColor={
useDefaultModel
? currentTheme.colors.white
: currentTheme.colors.mediumEmphasis
}
ios_backgroundColor={currentTheme.colors.elevation2}
/>
</View>
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
<Text
style={[
styles.description,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{useDefaultModel
? `Using ${DEFAULT_OPENROUTER_MODEL} (free automatic routing).`
: 'Use a custom OpenRouter model ID (useful for paid plans).'}
? `${t('ai_settings.using')} ${DEFAULT_OPENROUTER_MODEL} (free automatic routing).`
: t('ai_settings.paid_routing')}
</Text>
{!useDefaultModel && (
<TextInput
@ -296,8 +374,8 @@ const AISettingsScreen: React.FC = () => {
{
backgroundColor: currentTheme.colors.elevation2,
color: currentTheme.colors.highEmphasis,
borderColor: currentTheme.colors.elevation2
}
borderColor: currentTheme.colors.elevation2,
},
]}
value={customModel}
onChangeText={setCustomModel}
@ -312,7 +390,10 @@ const AISettingsScreen: React.FC = () => {
<View style={styles.buttonContainer}>
{!isKeySet ? (
<TouchableOpacity
style={[styles.saveButton, { backgroundColor: currentTheme.colors.primary }]}
style={[
styles.saveButton,
{ backgroundColor: currentTheme.colors.primary },
]}
onPress={handleSaveApiKey}
disabled={loading}
>
@ -329,7 +410,10 @@ const AISettingsScreen: React.FC = () => {
) : (
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.updateButton, { backgroundColor: currentTheme.colors.primary }]}
style={[
styles.updateButton,
{ backgroundColor: currentTheme.colors.primary },
]}
onPress={handleSaveApiKey}
disabled={loading}
>
@ -343,7 +427,10 @@ const AISettingsScreen: React.FC = () => {
</TouchableOpacity>
<TouchableOpacity
style={[styles.removeButton, { borderColor: currentTheme.colors.error }]}
style={[
styles.removeButton,
{ borderColor: currentTheme.colors.error },
]}
onPress={handleRemoveApiKey}
>
<MaterialIcons
@ -352,7 +439,12 @@ const AISettingsScreen: React.FC = () => {
color={currentTheme.colors.error}
style={{ marginRight: 8 }}
/>
<Text style={[styles.removeButtonText, { color: currentTheme.colors.error }]}>
<Text
style={[
styles.removeButtonText,
{ color: currentTheme.colors.error },
]}
>
{t('ai_settings.remove')}
</Text>
</TouchableOpacity>
@ -361,7 +453,10 @@ const AISettingsScreen: React.FC = () => {
</View>
<TouchableOpacity
style={[styles.getKeyButton, { backgroundColor: currentTheme.colors.elevation2 }]}
style={[
styles.getKeyButton,
{ backgroundColor: currentTheme.colors.elevation2 },
]}
onPress={handleGetApiKey}
>
<MaterialIcons
@ -370,7 +465,12 @@ const AISettingsScreen: React.FC = () => {
color={currentTheme.colors.primary}
style={{ marginRight: 8 }}
/>
<Text style={[styles.getKeyButtonText, { color: currentTheme.colors.primary }]}>
<Text
style={[
styles.getKeyButtonText,
{ color: currentTheme.colors.primary },
]}
>
{t('ai_settings.get_free_key')}
</Text>
</TouchableOpacity>
@ -378,51 +478,106 @@ const AISettingsScreen: React.FC = () => {
</View>
{/* Enable Toggle (top) */}
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>{t('ai_settings.enable_chat')}</Text>
<View
style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }]}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>
{t('ai_settings.enable_chat')}
</Text>
<Switch
value={!!settings.aiChatEnabled}
onValueChange={(v) => updateSetting('aiChatEnabled', v)}
trackColor={{ false: currentTheme.colors.elevation2, true: currentTheme.colors.primary }}
thumbColor={settings.aiChatEnabled ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis}
trackColor={{
false: currentTheme.colors.elevation2,
true: currentTheme.colors.primary,
}}
thumbColor={
settings.aiChatEnabled
? currentTheme.colors.white
: currentTheme.colors.mediumEmphasis
}
ios_backgroundColor={currentTheme.colors.elevation2}
/>
</View>
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis, marginTop: 8 }]}>{t('ai_settings.enable_chat_desc')}</Text>
<Text
style={[
styles.description,
{ color: currentTheme.colors.mediumEmphasis, marginTop: 8 },
]}
>
{t('ai_settings.enable_chat_desc')}
</Text>
</View>
{/* Status Card */}
{isKeySet && (
<View style={[styles.statusCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View
style={[
styles.statusCard,
{ backgroundColor: currentTheme.colors.elevation1 },
]}
>
<View style={styles.statusHeader}>
<MaterialIcons
name="check-circle"
size={24}
color={currentTheme.colors.success || '#4CAF50'}
/>
<Text style={[styles.statusTitle, { color: currentTheme.colors.success || '#4CAF50' }]}>
<Text
style={[
styles.statusTitle,
{ color: currentTheme.colors.success || '#4CAF50' },
]}
>
{t('ai_settings.chat_enabled')}
</Text>
</View>
<Text style={[styles.statusDescription, { color: currentTheme.colors.mediumEmphasis }]}>
<Text
style={[
styles.statusDescription,
{ color: currentTheme.colors.mediumEmphasis },
]}
>
{t('ai_settings.chat_enabled_desc')}
</Text>
</View>
)}
{/* Usage Info */}
<View style={[styles.usageCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.usageTitle, { color: currentTheme.colors.highEmphasis }]}>
<View
style={[
styles.usageCard,
{ backgroundColor: currentTheme.colors.elevation1 },
]}
>
<Text
style={[styles.usageTitle, { color: currentTheme.colors.highEmphasis }]}
>
{t('ai_settings.how_it_works')}
</Text>
<Text style={[styles.usageText, { color: currentTheme.colors.mediumEmphasis }]}>
<Text
style={[styles.usageText, { color: currentTheme.colors.mediumEmphasis }]}
>
{t('ai_settings.how_it_works_desc')}
</Text>
</View>
{/* OpenRouter branding */}
<View style={{ alignItems: 'center', marginTop: 16, marginBottom: 32 }}>
<SvgXml xml={OPENROUTER_SVG.replace(/CURRENTCOLOR/g, currentTheme.colors.mediumEmphasis)} width={180} height={60} />
<SvgXml
xml={OPENROUTER_SVG.replace(
/CURRENTCOLOR/g,
currentTheme.colors.mediumEmphasis,
)}
width={180}
height={60}
/>
</View>
</ScrollView>
<CustomAlert
@ -445,7 +600,8 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
paddingTop:
Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
},
backButton: {
flexDirection: 'row',