mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-07 18:49:45 +00:00
imrpoved toast.
This commit is contained in:
parent
d4af07938b
commit
b2ef847720
6 changed files with 62 additions and 149 deletions
40
package-lock.json
generated
40
package-lock.json
generated
|
|
@ -8,10 +8,12 @@
|
||||||
"name": "nuvio",
|
"name": "nuvio",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@backpackapp-io/react-native-toast": "^0.14.0",
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/blur": "^4.4.1",
|
"@react-native-community/blur": "^4.4.1",
|
||||||
|
"@react-native-community/netinfo": "^11.4.1",
|
||||||
"@react-native-community/slider": "^4.5.6",
|
"@react-native-community/slider": "^4.5.6",
|
||||||
"@react-native-picker/picker": "^2.11.0",
|
"@react-native-picker/picker": "^2.11.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||||
|
|
@ -2257,6 +2259,35 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@backpackapp-io/react-native-toast": {
|
||||||
|
"version": "0.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@backpackapp-io/react-native-toast/-/react-native-toast-0.14.0.tgz",
|
||||||
|
"integrity": "sha512-vt75tajD+kgHnKiuUlDIEPqqg1qtZ8PMG1mce4WJOEeF+BVbsFrSmoDGCRbPUHkq7TFuIc/Hn89lYAUqPlTFVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@backpackapp-io/react-native-toast": "^0.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-gesture-handler": ">=2.2.1",
|
||||||
|
"react-native-reanimated": ">=2.8.0",
|
||||||
|
"react-native-safe-area-context": ">=4.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@backpackapp-io/react-native-toast/node_modules/@backpackapp-io/react-native-toast": {
|
||||||
|
"version": "0.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@backpackapp-io/react-native-toast/-/react-native-toast-0.13.0.tgz",
|
||||||
|
"integrity": "sha512-kutFSE1vi77ybNV24JSnKQ4WUgWZ+LcYsrdAyl5ztEWGP7FRsfinsuaOrBQwDBGxm0rzMFeKM5K7fRHa/Hvy8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-gesture-handler": ">=2.2.1",
|
||||||
|
"react-native-reanimated": ">=2.8.0",
|
||||||
|
"react-native-safe-area-context": ">=4.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@callstack/react-theme-provider": {
|
"node_modules/@callstack/react-theme-provider": {
|
||||||
"version": "3.0.9",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz",
|
||||||
|
|
@ -3469,6 +3500,15 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-community/netinfo": {
|
||||||
|
"version": "11.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz",
|
||||||
|
"integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native": ">=0.59"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native-community/slider": {
|
"node_modules/@react-native-community/slider": {
|
||||||
"version": "4.5.6",
|
"version": "4.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.6.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@
|
||||||
"web": "expo start --web"
|
"web": "expo start --web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@backpackapp-io/react-native-toast": "^0.14.0",
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/blur": "^4.4.1",
|
"@react-native-community/blur": "^4.4.1",
|
||||||
|
"@react-native-community/netinfo": "^11.4.1",
|
||||||
"@react-native-community/slider": "^4.5.6",
|
"@react-native-community/slider": "^4.5.6",
|
||||||
"@react-native-picker/picker": "^2.11.0",
|
"@react-native-picker/picker": "^2.11.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
|
||||||
import { Animated, Easing, StyleSheet, Text, ViewStyle } from 'react-native';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export type ToastType = 'success' | 'error' | 'info';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
visible: boolean;
|
|
||||||
message: string;
|
|
||||||
type?: ToastType;
|
|
||||||
duration?: number; // ms
|
|
||||||
onHide?: () => void;
|
|
||||||
bottomOffset?: number; // extra offset above safe area / tab bar
|
|
||||||
containerStyle?: ViewStyle;
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorsByType: Record<ToastType, string> = {
|
|
||||||
success: 'rgba(46,160,67,0.95)',
|
|
||||||
error: 'rgba(229, 62, 62, 0.95)',
|
|
||||||
info: 'rgba(99, 102, 241, 0.95)',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ToastOverlay: React.FC<Props> = ({
|
|
||||||
visible,
|
|
||||||
message,
|
|
||||||
type = 'info',
|
|
||||||
duration = 1800,
|
|
||||||
onHide,
|
|
||||||
bottomOffset = 90,
|
|
||||||
containerStyle,
|
|
||||||
}) => {
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const opacity = useRef(new Animated.Value(0)).current;
|
|
||||||
const translateY = useRef(new Animated.Value(12)).current;
|
|
||||||
const hideTimer = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
// clear any existing timer
|
|
||||||
if (hideTimer.current) {
|
|
||||||
clearTimeout(hideTimer.current);
|
|
||||||
hideTimer.current = null;
|
|
||||||
}
|
|
||||||
opacity.setValue(0);
|
|
||||||
translateY.setValue(12);
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(opacity, { toValue: 1, duration: 160, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
|
||||||
Animated.timing(translateY, { toValue: 0, duration: 160, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
|
||||||
]).start(() => {
|
|
||||||
hideTimer.current = setTimeout(() => {
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(opacity, { toValue: 0, duration: 160, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
|
||||||
Animated.timing(translateY, { toValue: 12, duration: 160, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
|
||||||
]).start(() => {
|
|
||||||
if (onHide) onHide();
|
|
||||||
});
|
|
||||||
}, Math.max(800, duration));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// If toggled off externally, hide instantly
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(opacity, { toValue: 0, duration: 120, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
|
||||||
Animated.timing(translateY, { toValue: 12, duration: 120, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
|
||||||
]).start();
|
|
||||||
if (hideTimer.current) {
|
|
||||||
clearTimeout(hideTimer.current);
|
|
||||||
hideTimer.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (hideTimer.current) {
|
|
||||||
clearTimeout(hideTimer.current);
|
|
||||||
hideTimer.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [visible, duration, onHide, opacity, translateY]);
|
|
||||||
|
|
||||||
const bg = useMemo(() => colorsByType[type], [type]);
|
|
||||||
const bottom = (insets?.bottom || 0) + bottomOffset;
|
|
||||||
|
|
||||||
if (!visible && opacity.__getValue() === 0) {
|
|
||||||
// Avoid mounting when fully hidden to minimize layout cost
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
pointerEvents="none"
|
|
||||||
style={[styles.container, { bottom }, containerStyle, { opacity, transform: [{ translateY }] }]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.text, { backgroundColor: bg }]} numberOfLines={3}>
|
|
||||||
{message}
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
zIndex: 999,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
color: '#fff',
|
|
||||||
fontWeight: '700',
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingVertical: 8,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
borderRadius: 12,
|
|
||||||
overflow: 'hidden',
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ToastOverlay;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { NuvioHeader } from '../components/NuvioHeader';
|
||||||
import { Stream } from '../types/streams';
|
import { Stream } from '../types/streams';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { Toasts } from '@backpackapp-io/react-native-toast';
|
||||||
|
|
||||||
// Import screens with their proper types
|
// Import screens with their proper types
|
||||||
import HomeScreen from '../screens/HomeScreen';
|
import HomeScreen from '../screens/HomeScreen';
|
||||||
|
|
@ -1073,6 +1074,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</View>
|
</View>
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
|
<Toasts />
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useAccount } from '../contexts/AccountContext';
|
import { useAccount } from '../contexts/AccountContext';
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import ToastOverlay from '../components/common/ToastOverlay';
|
import { toast } from '@backpackapp-io/react-native-toast';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
@ -41,7 +41,7 @@ const AuthScreen: React.FC = () => {
|
||||||
const ctaTextTranslateY = useRef(new Animated.Value(0)).current;
|
const ctaTextTranslateY = useRef(new Animated.Value(0)).current;
|
||||||
const modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup
|
const modeAnim = useRef(new Animated.Value(0)).current; // 0 = signin, 1 = signup
|
||||||
const [switchWidth, setSwitchWidth] = useState(0);
|
const [switchWidth, setSwitchWidth] = useState(0);
|
||||||
const [toast, setToast] = useState<{ visible: boolean; message: string; type: 'success' | 'error' | 'info' }>({ visible: false, message: '', type: 'info' });
|
// Legacy local toast state removed in favor of global toast
|
||||||
const [headerHeight, setHeaderHeight] = useState(0);
|
const [headerHeight, setHeaderHeight] = useState(0);
|
||||||
const headerHideAnim = useRef(new Animated.Value(0)).current; // 0 visible, 1 hidden
|
const headerHideAnim = useRef(new Animated.Value(0)).current; // 0 visible, 1 hidden
|
||||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||||
|
|
@ -144,21 +144,21 @@ const AuthScreen: React.FC = () => {
|
||||||
if (!isEmailValid) {
|
if (!isEmailValid) {
|
||||||
const msg = 'Enter a valid email address';
|
const msg = 'Enter a valid email address';
|
||||||
setError(msg);
|
setError(msg);
|
||||||
showToast(msg, 'error');
|
toast.error(msg);
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
const msg = 'Password must be at least 6 characters';
|
const msg = 'Password must be at least 6 characters';
|
||||||
setError(msg);
|
setError(msg);
|
||||||
showToast(msg, 'error');
|
toast.error(msg);
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mode === 'signup' && !passwordsMatch) {
|
if (mode === 'signup' && !passwordsMatch) {
|
||||||
const msg = 'Passwords do not match';
|
const msg = 'Passwords do not match';
|
||||||
setError(msg);
|
setError(msg);
|
||||||
showToast(msg, 'error');
|
toast.error(msg);
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -167,11 +167,11 @@ const AuthScreen: React.FC = () => {
|
||||||
const err = mode === 'signin' ? await signIn(email.trim(), password) : await signUp(email.trim(), password);
|
const err = mode === 'signin' ? await signIn(email.trim(), password) : await signUp(email.trim(), password);
|
||||||
if (err) {
|
if (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
showToast(err, 'error');
|
toast.error(err);
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful';
|
const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful';
|
||||||
showToast(msg, 'success');
|
toast.success(msg);
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -184,9 +184,7 @@ const AuthScreen: React.FC = () => {
|
||||||
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any);
|
navigation.reset({ index: 0, routes: [{ name: 'MainTabs' as never }] } as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
// showToast helper replaced with direct calls to toast.* API
|
||||||
setToast({ visible: true, message, type });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
|
|
@ -521,15 +519,7 @@ const AuthScreen: React.FC = () => {
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
{/* Screen-level toast overlay so it is not clipped by the card */}
|
{/* Toasts rendered globally in App root */}
|
||||||
<ToastOverlay
|
|
||||||
visible={toast.visible}
|
|
||||||
message={toast.message}
|
|
||||||
type={toast.type}
|
|
||||||
duration={1600}
|
|
||||||
bottomOffset={(keyboardHeight > 0 ? keyboardHeight + 8 : 24)}
|
|
||||||
onHide={() => setToast(prev => ({ ...prev, visible: false }))}
|
|
||||||
/>
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ import type { Theme } from '../contexts/ThemeContext';
|
||||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import ToastOverlay from '../components/common/ToastOverlay';
|
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
|
||||||
import FirstTimeWelcome from '../components/FirstTimeWelcome';
|
import FirstTimeWelcome from '../components/FirstTimeWelcome';
|
||||||
import { imageCacheService } from '../services/imageCacheService';
|
import { imageCacheService } from '../services/imageCacheService';
|
||||||
|
|
||||||
|
|
@ -321,6 +321,13 @@ const HomeScreen = () => {
|
||||||
setHintVisible(true);
|
setHintVisible(true);
|
||||||
await AsyncStorage.removeItem('showLoginHintToastOnce');
|
await AsyncStorage.removeItem('showLoginHintToastOnce');
|
||||||
hideTimer = setTimeout(() => setHintVisible(false), 2000);
|
hideTimer = setTimeout(() => setHintVisible(false), 2000);
|
||||||
|
// Also show a global toast for consistency across screens
|
||||||
|
try {
|
||||||
|
toast('You can sign in anytime from Settings → Account', {
|
||||||
|
duration: 1600,
|
||||||
|
position: ToastPosition.BOTTOM,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
})();
|
})();
|
||||||
|
|
@ -728,15 +735,7 @@ const HomeScreen = () => {
|
||||||
disableIntervalMomentum={true}
|
disableIntervalMomentum={true}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
/>
|
/>
|
||||||
{/* One-time hint toast after skipping sign-in */}
|
{/* Toasts are rendered globally at root */}
|
||||||
<ToastOverlay
|
|
||||||
visible={hintVisible}
|
|
||||||
message="You can sign in anytime from Settings → Account"
|
|
||||||
type="info"
|
|
||||||
duration={1600}
|
|
||||||
bottomOffset={88}
|
|
||||||
onHide={() => setHintVisible(false)}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue