/ components / SmartBackButton.tsx
SmartBackButton.tsx
1 import React, { useState, useEffect } from 'react'; 2 import { 3 View, 4 TouchableOpacity, 5 StyleSheet, 6 Text, 7 Animated, 8 useColorScheme, 9 } from 'react-native'; 10 import { Ionicons } from '@expo/vector-icons'; 11 import { Colors, ColorScheme } from '@/constants/Colors'; 12 import { useNavigationHistory } from '@/hooks/useNavigationHistory'; 13 import { useRouter, usePathname } from 'expo-router'; 14 import { useHapticFeedback } from '@/utils/haptics'; 15 16 interface SmartBackButtonProps { 17 size?: number; 18 color?: string; 19 style?: any; 20 showLabel?: boolean; 21 customOnPress?: () => void; 22 disabled?: boolean; 23 } 24 25 const SmartBackButton: React.FC<SmartBackButtonProps> = ({ 26 size = 24, 27 color, 28 style, 29 showLabel = false, 30 customOnPress, 31 disabled = false, 32 }) => { 33 const colorScheme = useColorScheme() as ColorScheme; 34 const { handleBackPress, canGoBack, navigationState } = 35 useNavigationHistory(); 36 const router = useRouter(); 37 const pathname = usePathname(); 38 const haptics = useHapticFeedback(); 39 40 const [isPressed, setIsPressed] = useState(false); 41 const [backLabel, setBackLabel] = useState('Back'); 42 const scaleAnim = new Animated.Value(1); 43 44 const colors = Colors[colorScheme]; 45 const buttonColor = color || colors.text; 46 const isDisabled = disabled || !canGoBack; 47 48 useEffect(() => { 49 // Determine smart back label based on current context 50 const determineBackLabel = () => { 51 if (pathname.includes('/manga/') && pathname.includes('/chapter/')) { 52 return 'Manga'; 53 } else if (pathname.includes('/manga/')) { 54 // Check if we came from search, home, or bookmarks 55 const lastNonMangaRoute = navigationState.contextHistory 56 .slice() 57 .reverse() 58 .find( 59 (entry) => 60 !entry.path.includes('/manga/') || 61 (entry.path.includes('/manga/') && 62 entry.path.includes('/chapter/')) 63 ); 64 65 if (lastNonMangaRoute) { 66 if (lastNonMangaRoute.path === '/mangasearch') return 'Search'; 67 if (lastNonMangaRoute.path === '/') return 'Home'; 68 if (lastNonMangaRoute.path === '/bookmarks') return 'Library'; 69 if (lastNonMangaRoute.path === '/settings') return 'Settings'; 70 } 71 return 'Search'; // Default fallback for manga pages 72 } else if (pathname === '/settings') { 73 return 'Home'; 74 } else if (pathname === '/bookmarks') { 75 return 'Home'; 76 } else if (pathname === '/mangasearch') { 77 return 'Home'; 78 } 79 return 'Back'; 80 }; 81 82 setBackLabel(determineBackLabel()); 83 }, [pathname, navigationState]); 84 85 const handlePress = async () => { 86 if (isDisabled) return; 87 88 haptics.onPress(); 89 90 // Animate button press 91 Animated.sequence([ 92 Animated.timing(scaleAnim, { 93 toValue: 0.9, 94 duration: 100, 95 useNativeDriver: true, 96 }), 97 Animated.timing(scaleAnim, { 98 toValue: 1, 99 duration: 100, 100 useNativeDriver: true, 101 }), 102 ]).start(); 103 104 if (customOnPress) { 105 customOnPress(); 106 } else { 107 try { 108 await handleBackPress('tap'); 109 } catch (error) { 110 console.error('Smart back navigation failed:', error); 111 // Fallback to home if navigation fails 112 router.replace('/'); 113 } 114 } 115 }; 116 117 const getBackgroundColor = () => { 118 if (isDisabled) return 'transparent'; 119 if (isPressed) return colors.primary + '20'; 120 return 'transparent'; 121 }; 122 123 return ( 124 <TouchableOpacity 125 style={[ 126 styles.container, 127 { 128 backgroundColor: getBackgroundColor(), 129 opacity: isDisabled ? 0.3 : 1, 130 }, 131 style, 132 ]} 133 onPress={handlePress} 134 onPressIn={() => setIsPressed(true)} 135 onPressOut={() => setIsPressed(false)} 136 disabled={isDisabled} 137 accessibilityRole="button" 138 accessibilityLabel={`Go back to ${backLabel}`} 139 accessibilityHint={`Navigate back to the previous ${backLabel.toLowerCase()} page`} 140 > 141 <Animated.View 142 style={[styles.content, { transform: [{ scale: scaleAnim }] }]} 143 > 144 <View style={styles.iconContainer}> 145 <Ionicons name="arrow-back" size={size} color={buttonColor} /> 146 {navigationState.currentDepth > 1 && ( 147 <View 148 style={[ 149 styles.depthIndicator, 150 { backgroundColor: colors.primary }, 151 ]} 152 > 153 <Text style={[styles.depthText, { color: colors.background }]}> 154 {Math.min(navigationState.currentDepth - 1, 9)} 155 </Text> 156 </View> 157 )} 158 </View> 159 160 {showLabel && ( 161 <Text style={[styles.label, { color: buttonColor }]}> 162 {backLabel} 163 </Text> 164 )} 165 </Animated.View> 166 </TouchableOpacity> 167 ); 168 }; 169 170 const styles = StyleSheet.create({ 171 container: { 172 padding: 8, 173 borderRadius: 20, 174 justifyContent: 'center', 175 alignItems: 'center', 176 minWidth: 40, 177 minHeight: 40, 178 }, 179 content: { 180 flexDirection: 'row', 181 alignItems: 'center', 182 }, 183 iconContainer: { 184 position: 'relative', 185 justifyContent: 'center', 186 alignItems: 'center', 187 }, 188 depthIndicator: { 189 position: 'absolute', 190 top: -6, 191 right: -6, 192 width: 18, 193 height: 18, 194 borderRadius: 9, 195 justifyContent: 'center', 196 alignItems: 'center', 197 }, 198 depthText: { 199 fontSize: 10, 200 fontWeight: 'bold', 201 }, 202 label: { 203 marginLeft: 8, 204 fontSize: 16, 205 fontWeight: '500', 206 }, 207 }); 208 209 export default SmartBackButton;