mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
ui changes
This commit is contained in:
parent
9ba14f2f33
commit
0290b9318e
3 changed files with 122 additions and 18 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue