diff --git a/.gitignore b/.gitignore
index bbb0b26..86f4745 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,3 +56,4 @@ src/screens/xavio.md
/KSPlayer
/exobase
ffmpegreadme.md
+toast.md
diff --git a/package-lock.json b/package-lock.json
index 9d3c2b2..b468561 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 8a91332..a9931c0 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx
index e7d44c4..71bac4f 100644
--- a/src/components/home/ContentItem.tsx
+++ b/src/components/home/ContentItem.tsx
@@ -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);
diff --git a/src/hooks/useUpdatePopup.ts b/src/hooks/useUpdatePopup.ts
index 9885190..87ba6c7 100644
--- a/src/hooks/useUpdatePopup.ts
+++ b/src/hooks/useUpdatePopup.ts
@@ -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);
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
index 2592477..425e92f 100644
--- a/src/navigation/AppNavigator.tsx
+++ b/src/navigation/AppNavigator.tsx
@@ -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
-
+ {/* Global toast customization using ThemeContext */}
+ (
+
+ {props.text1}
+ {props.text2 ? (
+ {props.text2}
+ ) : null}
+
+ ),
+ success: (props: any) => (
+
+ {props.text1}
+ {props.text2 ? (
+ {props.text2}
+ ) : null}
+
+ ),
+ error: (props: any) => (
+
+ {props.text1}
+ {props.text2 ? (
+ {props.text2}
+ ) : null}
+
+ ),
+ }}
+ />
);
};
diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx
index a5e63d6..ba08816 100644
--- a/src/screens/AuthScreen.tsx
+++ b/src/screens/AuthScreen.tsx
@@ -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
diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx
index 854e526..4550a53 100644
--- a/src/screens/HomeScreen.tsx
+++ b/src/screens/HomeScreen.tsx
@@ -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 {}
})();
diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx
index 7e7d230..3514156 100644
--- a/src/screens/LibraryScreen.tsx
+++ b/src/screens/LibraryScreen.tsx
@@ -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
diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx
index e6b9568..0d959f4 100644
--- a/src/screens/SearchScreen.tsx
+++ b/src/screens/SearchScreen.tsx
@@ -531,12 +531,6 @@ const SearchScreen = () => {
)}
- {/* 'series'/'movie' text in original place */}
-
-
- {item.type === 'movie' ? 'MOVIE' : 'SERIES'}
-
-
{item.imdbRating && (
@@ -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,
diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index 43fd7fe..c301b46 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -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]);
diff --git a/src/screens/UpdateScreen.tsx b/src/screens/UpdateScreen.tsx
index 4e33cc8..9d9ea48 100644
--- a/src/screens/UpdateScreen.tsx
+++ b/src/screens/UpdateScreen.tsx
@@ -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 {}
}
}, []);