mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
Enhance navigation animations and transitions for improved user experience
This update modifies the AppNavigator to refine screen transition animations, including adjustments to animation types and durations based on platform. The SearchScreen now features smoother keyboard dismissal before navigation, and animations are added to enhance the visual experience during screen transitions. Additionally, the StreamsScreen is updated to prevent animation conflicts during exits, improving overall responsiveness and clarity in user interactions.
This commit is contained in:
parent
026c425660
commit
93a016b96e
3 changed files with 149 additions and 16 deletions
|
|
@ -780,8 +780,10 @@ const AppNavigator = () => {
|
||||||
component={StreamsScreen as any}
|
component={StreamsScreen as any}
|
||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'fade_from_bottom',
|
animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none',
|
||||||
animationDuration: Platform.OS === 'android' ? 200 : 300,
|
animationDuration: Platform.OS === 'android' ? 0 : 300,
|
||||||
|
gestureEnabled: true,
|
||||||
|
gestureDirection: Platform.OS === 'ios' ? 'vertical' : 'horizontal',
|
||||||
...(Platform.OS === 'ios' && { presentation: 'modal' }),
|
...(Platform.OS === 'ios' && { presentation: 'modal' }),
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
|
|
@ -825,8 +827,30 @@ const AppNavigator = () => {
|
||||||
name="Search"
|
name="Search"
|
||||||
component={SearchScreen as any}
|
component={SearchScreen as any}
|
||||||
options={{
|
options={{
|
||||||
animation: 'fade',
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
||||||
animationDuration: Platform.OS === 'android' ? 300 : 350,
|
animationDuration: Platform.OS === 'android' ? 250 : 350,
|
||||||
|
gestureEnabled: true,
|
||||||
|
gestureDirection: 'horizontal',
|
||||||
|
...(Platform.OS === 'android' && {
|
||||||
|
cardStyleInterpolator: ({ current, layouts }: any) => {
|
||||||
|
return {
|
||||||
|
cardStyle: {
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateX: current.progress.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [layouts.screen.width, 0],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
opacity: current.progress.interpolate({
|
||||||
|
inputRange: [0, 0.3, 1],
|
||||||
|
outputRange: [0, 0.85, 1],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,14 @@ const SearchScreen = () => {
|
||||||
setShowRecent(true);
|
setShowRecent(true);
|
||||||
loadRecentSearches();
|
loadRecentSearches();
|
||||||
} else {
|
} else {
|
||||||
navigation.goBack();
|
// Add a small delay to allow keyboard to dismiss smoothly before navigation
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
setTimeout(() => {
|
||||||
|
navigation.goBack();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
navigation.goBack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -497,7 +504,14 @@ const SearchScreen = () => {
|
||||||
const headerHeight = headerBaseHeight + topSpacing + 60;
|
const headerHeight = headerBaseHeight + topSpacing + 60;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
<Animated.View
|
||||||
|
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
||||||
|
entering={Platform.OS === 'android' ? SlideInRight.duration(250) : FadeIn.duration(350)}
|
||||||
|
exiting={Platform.OS === 'android' ?
|
||||||
|
FadeOut.duration(200).withInitialValues({ opacity: 1 }) :
|
||||||
|
FadeOut.duration(250)
|
||||||
|
}
|
||||||
|
>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
barStyle="light-content"
|
barStyle="light-content"
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
|
|
@ -656,7 +670,7 @@ const SearchScreen = () => {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { useSettings } from '../hooks/useSettings';
|
||||||
import QualityBadge from '../components/metadata/QualityBadge';
|
import QualityBadge from '../components/metadata/QualityBadge';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
FadeInDown,
|
FadeInDown,
|
||||||
SlideInDown,
|
SlideInDown,
|
||||||
withSpring,
|
withSpring,
|
||||||
|
|
@ -55,13 +56,14 @@ const DOLBY_ICON = 'https://upload.wikimedia.org/wikipedia/en/thumb/3/3f/Dolby_V
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
// Extracted Components
|
// Extracted Components
|
||||||
const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }: {
|
const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme, isExiting }: {
|
||||||
stream: Stream;
|
stream: Stream;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
index: number;
|
index: number;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
statusMessage?: string;
|
statusMessage?: string;
|
||||||
theme: any;
|
theme: any;
|
||||||
|
isExiting?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
||||||
|
|
||||||
|
|
@ -78,13 +80,92 @@ const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }:
|
||||||
const displayTitle = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream');
|
const displayTitle = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream');
|
||||||
const displayAddonName = isHDRezka ? '' : (stream.title || '');
|
const displayAddonName = isHDRezka ? '' : (stream.title || '');
|
||||||
|
|
||||||
// Animation delay based on index - stagger effect
|
// Animation delay based on index - stagger effect (only if not exiting)
|
||||||
const enterDelay = 100 + (index * 50);
|
const enterDelay = isExiting ? 0 : 100 + (index * 30);
|
||||||
|
|
||||||
|
// Use simple View when exiting to prevent animation conflicts
|
||||||
|
if (isExiting) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.streamCard,
|
||||||
|
isLoading && styles.streamCardLoading
|
||||||
|
]}
|
||||||
|
onPress={onPress}
|
||||||
|
disabled={isLoading}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.streamDetails}>
|
||||||
|
<View style={styles.streamNameRow}>
|
||||||
|
<View style={styles.streamTitleContainer}>
|
||||||
|
<Text style={[styles.streamName, { color: theme.colors.highEmphasis }]}>
|
||||||
|
{displayTitle}
|
||||||
|
</Text>
|
||||||
|
{displayAddonName && displayAddonName !== displayTitle && (
|
||||||
|
<Text style={[styles.streamAddonName, { color: theme.colors.mediumEmphasis }]}>
|
||||||
|
{displayAddonName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Show loading indicator if stream is loading */}
|
||||||
|
{isLoading && (
|
||||||
|
<View style={styles.loadingIndicator}>
|
||||||
|
<ActivityIndicator size="small" color={theme.colors.primary} />
|
||||||
|
<Text style={[styles.loadingText, { color: theme.colors.primary }]}>
|
||||||
|
{statusMessage || "Loading..."}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.streamMetaRow}>
|
||||||
|
{quality && quality >= "720" && (
|
||||||
|
<QualityBadge type="HD" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDolby && (
|
||||||
|
<QualityBadge type="VISION" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{size && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: theme.colors.darkGray }]}>
|
||||||
|
<Text style={[styles.chipText, { color: theme.colors.white }]}>{size}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDebrid && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: theme.colors.success }]}>
|
||||||
|
<Text style={[styles.chipText, { color: theme.colors.white }]}>DEBRID</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Special badge for HDRezka streams */}
|
||||||
|
{isHDRezka && (
|
||||||
|
<View style={[styles.chip, { backgroundColor: theme.colors.accent }]}>
|
||||||
|
<Text style={[styles.chipText, { color: theme.colors.white }]}>HDREZKA</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.streamAction}>
|
||||||
|
<MaterialIcons
|
||||||
|
name="play-arrow"
|
||||||
|
size={24}
|
||||||
|
color={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInDown.duration(300).delay(enterDelay).springify()}
|
entering={FadeInDown.duration(200).delay(enterDelay)}
|
||||||
layout={Layout.springify()}
|
layout={Layout.duration(200)}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -249,6 +330,9 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
// Add state for handling orientation transition
|
// Add state for handling orientation transition
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
|
// Add state to prevent animation conflicts during exit
|
||||||
|
const [isExiting, setIsExiting] = useState(false);
|
||||||
|
|
||||||
// Add timing logs
|
// Add timing logs
|
||||||
const [loadStartTime, setLoadStartTime] = useState(0);
|
const [loadStartTime, setLoadStartTime] = useState(0);
|
||||||
|
|
@ -400,20 +484,25 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
// Memoize handlers
|
// Memoize handlers
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
|
// Set exit state to prevent animation conflicts and hide content immediately
|
||||||
|
setIsExiting(true);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
headerOpacity.value = withTiming(0, { duration: 200 });
|
headerOpacity.value = withTiming(0, { duration: 100 });
|
||||||
heroScale.value = withTiming(0.95, { duration: 200 });
|
heroScale.value = withTiming(0.95, { duration: 100 });
|
||||||
filterOpacity.value = withTiming(0, { duration: 200 });
|
filterOpacity.value = withTiming(0, { duration: 100 });
|
||||||
};
|
};
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
// For series episodes, always replace current screen with metadata screen
|
// For series episodes, always replace current screen with metadata screen
|
||||||
if (type === 'series') {
|
if (type === 'series') {
|
||||||
|
// Immediate navigation for series
|
||||||
navigation.replace('Metadata', {
|
navigation.replace('Metadata', {
|
||||||
id: id,
|
id: id,
|
||||||
type: type
|
type: type
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// Immediate navigation for movies
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
}
|
}
|
||||||
}, [navigation, headerOpacity, heroScale, filterOpacity, type, id]);
|
}, [navigation, headerOpacity, heroScale, filterOpacity, type, id]);
|
||||||
|
|
@ -954,9 +1043,10 @@ export const StreamsScreen = () => {
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
statusMessage={undefined}
|
statusMessage={undefined}
|
||||||
theme={currentTheme}
|
theme={currentTheme}
|
||||||
|
isExiting={isExiting}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [handleStreamPress, currentTheme]);
|
}, [handleStreamPress, currentTheme, isExiting]);
|
||||||
|
|
||||||
const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => {
|
const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => {
|
||||||
const isProviderLoading = loadingProviders[section.addonId];
|
const isProviderLoading = loadingProviders[section.addonId];
|
||||||
|
|
@ -1035,6 +1125,11 @@ export const StreamsScreen = () => {
|
||||||
barStyle="light-content"
|
barStyle="light-content"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Instant overlay when exiting to prevent glitches */}
|
||||||
|
{isExiting && (
|
||||||
|
<View style={[StyleSheet.absoluteFill, { backgroundColor: colors.darkBackground, zIndex: 100 }]} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Transition overlay to mask orientation changes */}
|
{/* Transition overlay to mask orientation changes */}
|
||||||
{isTransitioning && (
|
{isTransitioning && (
|
||||||
<View style={styles.transitionOverlay}>
|
<View style={styles.transitionOverlay}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue