new toast library

This commit is contained in:
tapframe 2025-10-01 01:13:27 +05:30
parent 56654e1ced
commit 16c460cdc2
12 changed files with 251 additions and 84 deletions

1
.gitignore vendored
View file

@ -56,3 +56,4 @@ src/screens/xavio.md
/KSPlayer
/exobase
ffmpegreadme.md
toast.md

104
package-lock.json generated
View file

@ -72,9 +72,11 @@
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.3.0",
"react-native-video": "^6.12.0",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1"
"react-native-wheel-color-picker": "^1.3.1",
"toastify-react-native": "^7.2.3"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@ -12965,6 +12967,93 @@
"react-native": "*"
}
},
"node_modules/react-native-vector-icons": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",
"integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==",
"deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2",
"yargs": "^16.1.1"
},
"bin": {
"fa-upgrade.sh": "bin/fa-upgrade.sh",
"fa5-upgrade": "bin/fa5-upgrade.sh",
"fa6-upgrade": "bin/fa6-upgrade.sh",
"generate-icon": "bin/generate-icon.js"
}
},
"node_modules/react-native-vector-icons/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/react-native-vector-icons/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/react-native-vector-icons/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vector-icons/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-video": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.16.1.tgz",
@ -14853,6 +14942,19 @@
"node": ">=8.0"
}
},
"node_modules/toastify-react-native": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/toastify-react-native/-/toastify-react-native-7.2.3.tgz",
"integrity": "sha512-ngmpTKlTo0IRddwSsNWK+YKbB2veqotHy7Zpil4eksoLAlq0RPSgdVOk5QDEDUONJQ4r7ljGYeRW68KBztirsg==",
"license": "MIT",
"dependencies": {
"react-native-vector-icons": "*"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View file

@ -72,9 +72,11 @@
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.3.0",
"react-native-video": "^6.12.0",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1"
"react-native-wheel-color-picker": "^1.3.1",
"toastify-react-native": "^7.2.3"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { toast } from '@backpackapp-io/react-native-toast';
import { Toast } from 'toastify-react-native';
import { DeviceEventEmitter } from 'react-native';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Animated, Share } from 'react-native';
import { Image as ExpoImage } from 'expo-image';
@ -9,6 +9,8 @@ import { useSettings } from '../../hooks/useSettings';
import { catalogService, StreamingContent } from '../../services/catalogService';
import { DropUpMenu } from './DropUpMenu';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { storageService } from '../../services/storageService';
import { TraktService } from '../../services/traktService';
interface ContentItemProps {
item: StreamingContent;
@ -116,28 +118,52 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
onPress(item.id, item.type);
}, [item.id, item.type, onPress]);
const handleOptionSelect = useCallback((option: string) => {
const handleOptionSelect = useCallback(async (option: string) => {
switch (option) {
case 'library':
if (inLibrary) {
catalogService.removeFromLibrary(item.type, item.id);
toast('Removed from Library', { duration: 1200 });
Toast.info('Removed from Library');
} else {
catalogService.addToLibrary(item);
toast('Added to Library', { duration: 1200 });
Toast.success('Added to Library');
}
break;
case 'watched': {
setIsWatched(prevWatched => {
const newWatched = !prevWatched;
AsyncStorage.setItem(`watched:${item.type}:${item.id}`, newWatched ? 'true' : 'false');
toast(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', { duration: 1200 });
// Fire a custom event so other screens can update
setTimeout(() => {
DeviceEventEmitter.emit('watchedStatusChanged');
}, 100);
return newWatched;
});
const targetWatched = !isWatched;
setIsWatched(targetWatched);
try {
await AsyncStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
} catch {}
Toast.info(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched');
setTimeout(() => {
DeviceEventEmitter.emit('watchedStatusChanged');
}, 100);
// Best-effort sync: record local progress and push to Trakt if available
if (targetWatched) {
try {
await storageService.setWatchProgress(
item.id,
item.type,
{ currentTime: 1, duration: 1, lastUpdated: Date.now() },
undefined,
{ forceNotify: true, forceWrite: true }
);
} catch {}
if (item.type === 'movie') {
try {
const trakt = TraktService.getInstance();
if (await trakt.isAuthenticated()) {
await trakt.addToWatchedMovies(item.id);
try {
await storageService.updateTraktSyncStatus(item.id, item.type, true, 100);
} catch {}
}
} catch {}
}
}
setMenuVisible(false);
break;
}
@ -153,7 +179,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
break;
}
}
}, [item, inLibrary]);
}, [item, inLibrary, isWatched]);
const handleMenuClose = useCallback(() => {
setMenuVisible(false);

View file

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { Platform } from 'react-native';
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
import { Toast } from 'toastify-react-native';
import UpdateService, { UpdateInfo } from '../services/updateService';
import AsyncStorage from '@react-native-async-storage/async-storage';
@ -78,19 +78,13 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
// The app will automatically reload with the new version
console.log('Update installed successfully');
} else {
toast('Unable to install the update. Please try again later or check your internet connection.', {
duration: 3000,
position: ToastPosition.TOP,
});
Toast.error('Unable to install the update. Please try again later or check your internet connection.');
// Show popup again after failed installation
setShowUpdatePopup(true);
}
} catch (error) {
if (__DEV__) console.error('Error installing update:', error);
toast('An error occurred while installing the update. Please try again later.', {
duration: 3000,
position: ToastPosition.TOP,
});
Toast.error('An error occurred while installing the update. Please try again later.');
// Show popup again after error
setShowUpdatePopup(true);
} finally {
@ -141,12 +135,7 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
(async () => {
try { await AsyncStorage.setItem(UPDATE_BADGE_KEY, 'true'); } catch {}
})();
try {
toast('Update available — go to Settings → App Updates', {
duration: 3000,
position: ToastPosition.TOP,
});
} catch {}
try { Toast.info('Update available — go to Settings → App Updates'); } catch {}
setShowUpdatePopup(false);
} else {
setShowUpdatePopup(true);

View file

@ -15,7 +15,7 @@ import { HeaderVisibility } from '../contexts/HeaderVisibility';
import { Stream } from '../types/streams';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
import { Toasts } from '@backpackapp-io/react-native-toast';
import ToastManager from 'toastify-react-native';
import { PostHogProvider } from 'posthog-react-native';
// Import screens with their proper types
@ -889,6 +889,7 @@ const customFadeInterpolator = ({ current, layouts }: any) => {
const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => {
const { currentTheme } = useTheme();
const { user, loading } = useAccount();
const insets = useSafeAreaInsets();
// Handle Android-specific optimizations
useEffect(() => {
@ -1344,7 +1345,85 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
</Stack.Navigator>
</View>
</PaperProvider>
<Toasts />
{/* Global toast customization using ThemeContext */}
<ToastManager
position="top"
useModal={false}
theme={'dark'}
// Dimensions
width={'90%'}
minHeight={61}
// Icon defaults
iconFamily="MaterialIcons"
iconSize={22}
icons={{
success: 'check-circle',
error: 'error',
info: 'info',
warn: 'warning',
default: 'notifications',
}}
// Close icon defaults
showCloseIcon={true}
closeIcon={'close'}
closeIconFamily={'MaterialIcons'}
closeIconSize={18}
// Spacing (ensure below safe area)
topOffset={Math.max(8, insets.top + 8)}
bottomOffset={40}
// Styles bound to ThemeContext
style={{
backgroundColor: currentTheme.colors.darkBackground,
borderRadius: 12,
paddingVertical: 12,
paddingHorizontal: 14,
}}
textStyle={{
color: currentTheme.colors.highEmphasis,
fontWeight: '600',
}}
config={{
default: (props: any) => (
<View style={{
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 12,
padding: 12,
width: '100%'
}}>
<Text style={{ color: currentTheme.colors.highEmphasis, fontWeight: '700' }}>{props.text1}</Text>
{props.text2 ? (
<Text style={{ color: currentTheme.colors.mediumEmphasis, marginTop: 4 }}>{props.text2}</Text>
) : null}
</View>
),
success: (props: any) => (
<View style={{
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 12,
padding: 12,
width: '100%'
}}>
<Text style={{ color: currentTheme.colors.success || '#4CAF50', fontWeight: '800' }}>{props.text1}</Text>
{props.text2 ? (
<Text style={{ color: currentTheme.colors.mediumEmphasis, marginTop: 4 }}>{props.text2}</Text>
) : null}
</View>
),
error: (props: any) => (
<View style={{
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 12,
padding: 12,
width: '100%'
}}>
<Text style={{ color: currentTheme.colors.error || '#ff4444', fontWeight: '800' }}>{props.text1}</Text>
{props.text2 ? (
<Text style={{ color: currentTheme.colors.mediumEmphasis, marginTop: 4 }}>{props.text2}</Text>
) : null}
</View>
),
}}
/>
</SafeAreaProvider>
);
};

View file

@ -7,7 +7,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useAccount } from '../contexts/AccountContext';
import { useNavigation, useRoute } from '@react-navigation/native';
import * as Haptics from 'expo-haptics';
import { toast } from '@backpackapp-io/react-native-toast';
import ToastManager, { Toast } from 'toastify-react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { width, height } = Dimensions.get('window');
@ -144,21 +144,21 @@ const AuthScreen: React.FC = () => {
if (!isEmailValid) {
const msg = 'Enter a valid email address';
setError(msg);
toast.error(msg);
Toast.error(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return;
}
if (!isPasswordValid) {
const msg = 'Password must be at least 6 characters';
setError(msg);
toast.error(msg);
Toast.error(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return;
}
if (mode === 'signup' && !passwordsMatch) {
const msg = 'Passwords do not match';
setError(msg);
toast.error(msg);
Toast.error(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
return;
}
@ -167,11 +167,11 @@ const AuthScreen: React.FC = () => {
const err = mode === 'signin' ? await signIn(email.trim(), password) : await signUp(email.trim(), password);
if (err) {
setError(err);
toast.error(err);
Toast.error(err);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
} else {
const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful';
toast.success(msg);
Toast.success(msg);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
// Navigate to main tabs after successful authentication

View file

@ -58,7 +58,7 @@ import { useLoading } from '../contexts/LoadingContext';
import * as ScreenOrientation from 'expo-screen-orientation';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
import { Toast } from 'toastify-react-native';
import FirstTimeWelcome from '../components/FirstTimeWelcome';
import { imageCacheService } from '../services/imageCacheService';
import { HeaderVisibility } from '../contexts/HeaderVisibility';
@ -341,12 +341,7 @@ const HomeScreen = () => {
await AsyncStorage.removeItem('showLoginHintToastOnce');
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 {}
try { Toast.info('You can sign in anytime from Settings → Account', 'bottom'); } catch {}
}
} catch {}
})();

View file

@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { DeviceEventEmitter } from 'react-native';
import { Share } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { toast } from '@backpackapp-io/react-native-toast';
import { Toast } from 'toastify-react-native';
import DropUpMenu from '../components/home/DropUpMenu';
import {
View,
@ -1002,11 +1002,11 @@ const LibraryScreen = () => {
case 'library': {
try {
await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id);
toast('Removed from Library', { duration: 1200 });
Toast.info('Removed from Library');
setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type)));
setMenuVisible(false);
} catch (error) {
toast('Failed to update Library', { duration: 1200 });
Toast.error('Failed to update Library');
}
break;
}
@ -1016,7 +1016,7 @@ const LibraryScreen = () => {
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
const newWatched = !selectedItem.watched;
await AsyncStorage.setItem(key, newWatched ? 'true' : 'false');
toast(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', { duration: 1200 });
Toast.info(newWatched ? 'Marked as Watched' : 'Marked as Unwatched');
// Instantly update local state
setLibraryItems(prev => prev.map(item =>
item.id === selectedItem.id && item.type === selectedItem.type

View file

@ -531,12 +531,6 @@ const SearchScreen = () => {
<MaterialIcons name="check-circle" size={20} color={currentTheme.colors.success || '#4CAF50'} />
</View>
)}
{/* 'series'/'movie' text in original place */}
<View style={styles.itemTypeContainer}>
<Text style={[styles.itemTypeText, { color: currentTheme.colors.white }]}>
{item.type === 'movie' ? 'MOVIE' : 'SERIES'}
</Text>
</View>
{item.imdbRating && (
<View style={styles.ratingContainer}>
<MaterialIcons name="star" size={12} color="#FFC107" />
@ -1038,19 +1032,6 @@ const styles = StyleSheet.create({
marginBottom: 16,
borderRadius: 4,
},
itemTypeContainer: {
position: 'absolute',
top: 8,
left: 8,
backgroundColor: 'rgba(0,0,0,0.7)',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
itemTypeText: {
fontSize: isTablet ? 7 : 8,
fontWeight: '700',
},
ratingContainer: {
position: 'absolute',
bottom: 8,

View file

@ -47,7 +47,7 @@ import QualityBadge from '../components/metadata/QualityBadge';
import { logger } from '../utils/logger';
import { isMkvStream } from '../utils/mkvDetection';
import CustomAlert from '../components/CustomAlert';
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
import { Toast } from 'toastify-react-native';
import { useDownloads } from '../contexts/DownloadsContext';
const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906';
@ -233,10 +233,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
// Use toast for Android, custom alert for iOS
if (Platform.OS === 'android') {
toast('Stream URL copied to clipboard!', {
duration: 2000,
position: ToastPosition.BOTTOM,
});
Toast.success('Stream URL copied to clipboard!', 'bottom');
} else {
// iOS uses custom alert
setTimeout(() => {
@ -246,10 +243,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
} catch (error) {
// Fallback: show URL in alert if clipboard fails
if (Platform.OS === 'android') {
toast(`Stream URL: ${stream.url}`, {
duration: 3000,
position: ToastPosition.BOTTOM,
});
Toast.info(`Stream URL: ${stream.url}`, 'bottom');
} else {
setTimeout(() => {
showAlert('Stream URL', stream.url);
@ -322,7 +316,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
url,
headers: (stream.headers as any) || undefined,
});
toast('Download started', { duration: 1500, position: ToastPosition.BOTTOM });
Toast.success('Download started', 'bottom');
} catch {}
}, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title]);

View file

@ -11,7 +11,7 @@ import {
Dimensions,
Linking
} from 'react-native';
import { toast, ToastPosition } from '@backpackapp-io/react-native-toast';
import { Toast } from 'toastify-react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
@ -152,9 +152,7 @@ const UpdateScreen: React.FC = () => {
// Also refresh GitHub section on mount (works in dev and prod)
try { github.refresh(); } catch {}
if (Platform.OS === 'android') {
try {
toast('Checking for updates…', { duration: 1200, position: ToastPosition.TOP });
} catch {}
try { Toast.info('Checking for updates…'); } catch {}
}
}, []);