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 {} } }, []);