ui changes

This commit is contained in:
tapframe 2025-09-11 17:27:51 +05:30
parent 9ba14f2f33
commit 0290b9318e
3 changed files with 122 additions and 18 deletions

View file

@ -1254,11 +1254,11 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
name="AIChat" name="AIChat"
component={AIChatScreen} component={AIChatScreen}
options={{ options={{
animation: Platform.OS === 'android' ? 'slide_from_bottom' : 'slide_from_bottom', animation: Platform.OS === 'android' ? 'none' : 'slide_from_bottom',
animationDuration: Platform.OS === 'android' ? 250 : 300, animationDuration: Platform.OS === 'android' ? 220 : 300,
presentation: 'modal', presentation: 'modal',
gestureEnabled: true, gestureEnabled: true,
gestureDirection: 'vertical', gestureDirection: Platform.OS === 'android' ? 'vertical' : 'vertical',
headerShown: false, headerShown: false,
contentStyle: { contentStyle: {
backgroundColor: currentTheme.colors.darkBackground, backgroundColor: currentTheme.colors.darkBackground,

View file

@ -14,9 +14,12 @@ import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
} from 'react-native'; } from 'react-native';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { Image } from 'expo-image';
import { BlurView as ExpoBlurView } from 'expo-blur';
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, generateConversationStarters } from '../services/aiService'; import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, generateConversationStarters } from '../services/aiService';
import { tmdbService } from '../services/tmdbService'; import { tmdbService } from '../services/tmdbService';
@ -27,7 +30,8 @@ import Animated, {
withSpring, withSpring,
withTiming, withTiming,
interpolate, interpolate,
Extrapolate Extrapolate,
runOnJS
} from 'react-native-reanimated'; } from 'react-native-reanimated';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
@ -102,10 +106,18 @@ const ChatBubble: React.FC<ChatBubbleProps> = ({ message, isLast }) => {
styles.userBubble, styles.userBubble,
{ backgroundColor: currentTheme.colors.primary } { backgroundColor: currentTheme.colors.primary }
] : [ ] : [
styles.assistantBubble, styles.assistantBubble,
{ backgroundColor: currentTheme.colors.elevation2 } { backgroundColor: 'transparent' }
] ]
]}> ]}>
{!isUser && (
<View style={styles.assistantBlurBackdrop} pointerEvents="none">
{Platform.OS === 'ios'
? <ExpoBlurView intensity={70} tint="dark" style={StyleSheet.absoluteFill} />
: <CommunityBlurView blurAmount={16} blurRadius={8} style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.50)' }]} />
</View>
)}
{isUser ? ( {isUser ? (
<Text style={[styles.messageText, { color: 'white' }]}> <Text style={[styles.messageText, { color: 'white' }]}>
{message.content} {message.content}
@ -281,6 +293,18 @@ const AIChatScreen: React.FC = () => {
const [context, setContext] = useState<ContentContext | null>(null); const [context, setContext] = useState<ContentContext | null>(null);
const [isLoadingContext, setIsLoadingContext] = useState(true); const [isLoadingContext, setIsLoadingContext] = useState(true);
const [suggestions, setSuggestions] = useState<string[]>([]); const [suggestions, setSuggestions] = useState<string[]>([]);
const [backdropUrl, setBackdropUrl] = useState<string | null>(null);
// Ensure Android cleans up heavy image resources when leaving the screen to avoid flash on back
useFocusEffect(
React.useCallback(() => {
return () => {
if (Platform.OS === 'android') {
setBackdropUrl(null);
}
};
}, [])
);
const scrollViewRef = useRef<ScrollView>(null); const scrollViewRef = useRef<ScrollView>(null);
const inputRef = useRef<TextInput>(null); const inputRef = useRef<TextInput>(null);
@ -288,11 +312,21 @@ const AIChatScreen: React.FC = () => {
// Animation values // Animation values
const headerOpacity = useSharedValue(1); const headerOpacity = useSharedValue(1);
const inputContainerY = useSharedValue(0); const inputContainerY = useSharedValue(0);
// Android full-screen modal fade
const modalOpacity = useSharedValue(Platform.OS === 'android' ? 0 : 1);
useEffect(() => { useEffect(() => {
loadContext(); loadContext();
}, []); }, []);
// Animate in on Android for full-screen modal feel
useEffect(() => {
if (Platform.OS === 'android') {
// Use spring to avoid jank on some devices
modalOpacity.value = withSpring(1, { damping: 20, stiffness: 140 });
}
}, [modalOpacity]);
useEffect(() => { useEffect(() => {
if (context && messages.length === 0) { if (context && messages.length === 0) {
// Generate conversation starters // Generate conversation starters
@ -320,6 +354,10 @@ const AIChatScreen: React.FC = () => {
const movieContext = createMovieContext(movieData); const movieContext = createMovieContext(movieData);
setContext(movieContext); setContext(movieContext);
try {
const path = movieData.backdrop_path || movieData.poster_path || null;
if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`);
} catch {}
} else { } else {
// Series: resolve TMDB numeric id first (contentId may be IMDb/stremio id) // Series: resolve TMDB numeric id first (contentId may be IMDb/stremio id)
let tmdbNumericId: number | null = null; let tmdbNumericId: number | null = null;
@ -343,6 +381,10 @@ const AIChatScreen: React.FC = () => {
]); ]);
if (!showData) throw new Error('Unable to load TV show details'); if (!showData) throw new Error('Unable to load TV show details');
try {
const path = showData.backdrop_path || showData.poster_path || null;
if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`);
} catch {}
if (episodeData && seasonNumber && episodeNumber) { if (episodeData && seasonNumber && episodeNumber) {
const episodeContext = createEpisodeContext( const episodeContext = createEpisodeContext(
@ -533,21 +575,44 @@ const AIChatScreen: React.FC = () => {
} }
return ( return (
<Animated.View style={{ flex: 1, opacity: modalOpacity }}>
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
{backdropUrl && (
<View style={StyleSheet.absoluteFill} pointerEvents="none">
<Image
source={{ uri: backdropUrl }}
style={StyleSheet.absoluteFill}
contentFit="cover"
recyclingKey={backdropUrl || undefined}
/>
{Platform.OS === 'ios'
? <ExpoBlurView intensity={60} tint="dark" style={StyleSheet.absoluteFill} />
: <CommunityBlurView blurAmount={12} blurRadius={6} 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" /> <StatusBar barStyle="light-content" />
{/* Header */} {/* Header */}
<Animated.View style={[ <Animated.View style={[
styles.header, styles.header,
{ {
backgroundColor: currentTheme.colors.darkBackground, backgroundColor: 'transparent',
paddingTop: insets.top paddingTop: Platform.OS === 'ios' ? 8 : insets.top
}, },
headerAnimatedStyle headerAnimatedStyle
]}> ]}>
<View style={styles.headerContent}> <View style={styles.headerContent}>
<TouchableOpacity <TouchableOpacity
onPress={() => navigation.goBack()} onPress={() => {
if (Platform.OS === 'android') {
modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => {
if (finished) runOnJS(navigation.goBack)();
});
} else {
navigation.goBack();
}
}}
style={styles.backButton} style={styles.backButton}
> >
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} /> <MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
@ -571,13 +636,16 @@ const AIChatScreen: React.FC = () => {
{/* Chat Messages */} {/* Chat Messages */}
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.chatContainer} style={styles.chatContainer}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={insets.top + 60} keyboardVerticalOffset={Platform.OS === 'ios' ? insets.top + 60 : 0}
> >
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
style={styles.messagesContainer} style={styles.messagesContainer}
contentContainerStyle={styles.messagesContent} contentContainerStyle={[
styles.messagesContent,
{ paddingBottom: 120 + insets.bottom }
]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
@ -637,10 +705,19 @@ const AIChatScreen: React.FC = () => {
{/* Input Container */} {/* Input Container */}
<Animated.View style={[ <Animated.View style={[
styles.inputContainer, styles.inputContainer,
{ backgroundColor: currentTheme.colors.darkBackground }, {
backgroundColor: 'transparent',
paddingBottom: 12 + insets.bottom
},
inputAnimatedStyle inputAnimatedStyle
]}> ]}>
<View style={[styles.inputWrapper, { backgroundColor: currentTheme.colors.elevation1 }]}> <View style={[styles.inputWrapper, { backgroundColor: 'transparent' }]}>
<View style={styles.inputBlurBackdrop} pointerEvents="none">
{Platform.OS === 'ios'
? <ExpoBlurView intensity={50} tint="dark" style={StyleSheet.absoluteFill} />
: <CommunityBlurView blurAmount={10} blurRadius={4} 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 <TextInput
ref={inputRef} ref={inputRef}
style={[ style={[
@ -679,6 +756,7 @@ const AIChatScreen: React.FC = () => {
</Animated.View> </Animated.View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>
</Animated.View>
); );
}; };
@ -827,12 +905,17 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 12,
borderRadius: 20, borderRadius: 20,
overflow: 'hidden',
}, },
userBubble: { userBubble: {
borderBottomRightRadius: 4, borderBottomRightRadius: 20,
}, },
assistantBubble: { assistantBubble: {
borderBottomLeftRadius: 4, borderBottomLeftRadius: 20,
},
assistantBlurBackdrop: {
...StyleSheet.absoluteFillObject,
borderRadius: 20,
}, },
messageText: { messageText: {
fontSize: 16, fontSize: 16,
@ -876,6 +959,11 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 8, paddingVertical: 8,
gap: 12, gap: 12,
overflow: 'hidden'
},
inputBlurBackdrop: {
...StyleSheet.absoluteFillObject,
borderRadius: 24,
}, },
textInput: { textInput: {
flex: 1, flex: 1,

View file

@ -132,7 +132,16 @@ CRITICAL INSTRUCTIONS:
3. If Release Status shows "RELEASED AND AVAILABLE FOR VIEWING", the content IS AVAILABLE. Do not say it's "upcoming" or "unreleased". 3. If Release Status shows "RELEASED AND AVAILABLE FOR VIEWING", the content IS AVAILABLE. Do not say it's "upcoming" or "unreleased".
4. Compare air dates to today's date (${currentDate}) to determine if something has already aired. 4. Compare air dates to today's date (${currentDate}) to determine if something has already aired.
5. Base ALL responses on the verified information above, NOT on your training knowledge. 5. Base ALL responses on the verified information above, NOT on your training knowledge.
6. If asked about release dates or availability, refer ONLY to the database information provided.`; 6. If asked about release dates or availability, refer ONLY to the database information provided.
FORMATTING RULES (use Markdown):
- Use short paragraphs separated by blank lines.
- Use clear headings (## or ###) when helpful.
- Use bullet lists for points, character lists, and steps.
- Add a blank line before and after lists and headings.
- Keep lines concise; avoid giant unbroken blocks of text.
- Wrap inline code/terms with backticks only when appropriate.
`;
} else { } else {
const movie = context as MovieContext; const movie = context as MovieContext;
const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
@ -164,6 +173,13 @@ CRITICAL INSTRUCTIONS:
5. If asked about release dates or availability, refer ONLY to the database information provided. 5. If asked about release dates or availability, refer ONLY to the database information provided.
6. You can discuss themes, production, performances, and high-level plot setup without revealing twists, surprises, or outcomes. 6. You can discuss themes, production, performances, and high-level plot setup without revealing twists, surprises, or outcomes.
FORMATTING RULES (use Markdown):
- Use short paragraphs separated by blank lines.
- Use clear headings (## or ###) when helpful.
- Use bullet lists for points and steps.
- Add a blank line before and after lists and headings.
- Keep lines concise; avoid giant unbroken blocks of text.
Answer questions about this movie using only the verified database information above, including plot analysis, character development, themes, cinematography, production notes, and trivia. Provide detailed, informative responses while remaining spoiler-safe.`; Answer questions about this movie using only the verified database information above, including plot analysis, character development, themes, cinematography, production notes, and trivia. Provide detailed, informative responses while remaining spoiler-safe.`;
} }
} }