diff --git a/package-lock.json b/package-lock.json index 76bf03f..0adc2fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15059,17 +15059,6 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "license": "MIT" }, - "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", diff --git a/src/components/CustomAlert.tsx b/src/components/CustomAlert.tsx new file mode 100644 index 0000000..7540ed5 --- /dev/null +++ b/src/components/CustomAlert.tsx @@ -0,0 +1,166 @@ +import React, { useEffect } from 'react'; +import { + Modal, + View, + Text, + StyleSheet, + Pressable, + TouchableOpacity, + useColorScheme, + Platform, +} from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated'; +import { useTheme } from '../contexts/ThemeContext'; + +interface CustomAlertProps { + visible: boolean; + title: string; + message: string; + onClose: () => void; + actions?: Array<{ + label: string; + onPress: () => void; + style?: object; + }>; +} + +export const CustomAlert = ({ + visible, + title, + message, + onClose, + actions = [ + { label: 'OK', onPress: onClose } + ], +}: CustomAlertProps) => { + const opacity = useSharedValue(0); + const scale = useSharedValue(0.95); + const isDarkMode = useColorScheme() === 'dark'; + const { currentTheme } = useTheme(); + const themeColors = currentTheme.colors; + + useEffect(() => { + const animDuration = 120; + if (visible) { + opacity.value = withTiming(1, { duration: animDuration }); + scale.value = withTiming(1, { duration: animDuration }); + } else { + opacity.value = withTiming(0, { duration: animDuration }); + scale.value = withTiming(0.95, { duration: animDuration }); + } + }, [visible]); + + const overlayStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + const alertStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + opacity: opacity.value, + })); + + const backgroundColor = isDarkMode ? themeColors.darkBackground : themeColors.elevation2 || '#FFFFFF'; + const textColor = isDarkMode ? themeColors.white : themeColors.black || '#000000'; + const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; + + return ( + + + + + + {title} + {message} + + {actions.map((action, idx) => ( + { + action.onPress(); + onClose(); + }} + > + {action.label} + + ))} + + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + overlayPressable: { + ...StyleSheet.absoluteFillObject, + }, + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + alertContainer: { + minWidth: 280, + maxWidth: '85%', + borderRadius: 20, + padding: 24, + borderWidth: 1, + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 8, + }, + android: { + elevation: 8, + }, + }), + }, + title: { + fontSize: 18, + fontWeight: '700', + marginBottom: 12, + textAlign: 'center', + }, + message: { + fontSize: 16, + marginBottom: 20, + textAlign: 'center', + }, + actionsRow: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 12, + }, + actionButton: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + }, + lastActionButton: { + // Optionally style the last button differently + }, + actionText: { + fontSize: 16, + fontWeight: '600', + }, +}); + +export default CustomAlert; diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 48bdfbc..90f4264 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -7,7 +7,6 @@ import { Dimensions, AppState, AppStateStatus, - Alert, ActivityIndicator } from 'react-native'; import { FlashList } from '@shopify/flash-list'; @@ -24,6 +23,7 @@ import { logger } from '../../utils/logger'; import * as Haptics from 'expo-haptics'; import { TraktService } from '../../services/traktService'; import { stremioService } from '../../services/stremioService'; +import CustomAlert from '../../components/CustomAlert'; // Define interface for continue watching items interface ContinueWatchingItem extends StreamingContent { @@ -96,6 +96,12 @@ const ContinueWatchingSection = React.forwardRef((props, re const [deletingItemId, setDeletingItemId] = useState(null); const longPressTimeoutRef = useRef(null); + // Alert state for CustomAlert + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); + // Use a ref to track if a background refresh is in progress to avoid state updates const isRefreshingRef = useRef(false); @@ -516,80 +522,51 @@ const ContinueWatchingSection = React.forwardRef((props, re // Ignore haptic errors } - // Show confirmation alert - Alert.alert( - "Remove from Continue Watching", - `Remove "${item.name}" from your continue watching list?`, - [ - { - text: "Cancel", - style: "cancel" - }, - { - text: "Remove", - style: "destructive", - onPress: async () => { - setDeletingItemId(item.id); - try { - // Trigger haptic feedback for confirmation - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - - // Remove all watch progress for this content (all episodes if series) - await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true }); - - // Also remove from Trakt playback queue if authenticated - const traktService = TraktService.getInstance(); - const isAuthed = await traktService.isAuthenticated(); - logger.log(`🔍 [ContinueWatching] Trakt authentication status: ${isAuthed}`); - - if (isAuthed) { - logger.log(`đŸ—‘ī¸ [ContinueWatching] Removing Trakt history for ${item.id}`); - let traktResult = false; - - if (item.type === 'movie') { - logger.log(`đŸŽŦ [ContinueWatching] Removing movie from Trakt history: ${item.name}`); - traktResult = await traktService.removeMovieFromHistory(item.id); - } else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) { - logger.log(`đŸ“ē [ContinueWatching] Removing specific episode from Trakt history: ${item.name} S${item.season}E${item.episode}`); - traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode); - } else { - logger.log(`đŸ“ē [ContinueWatching] Removing entire show from Trakt history: ${item.name} (no specific episode info)`); - traktResult = await traktService.removeShowFromHistory(item.id); - } - - logger.log(`✅ [ContinueWatching] Trakt removal result: ${traktResult}`); + setAlertTitle('Remove from Continue Watching'); + setAlertMessage(`Remove "${item.name}" from your continue watching list?`); + setAlertActions([ + { + label: 'Cancel', + style: { color: '#888' }, + onPress: () => {}, + }, + { + label: 'Remove', + style: { color: currentTheme.colors.error }, + onPress: async () => { + setDeletingItemId(item.id); + try { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true }); + const traktService = TraktService.getInstance(); + const isAuthed = await traktService.isAuthenticated(); + if (isAuthed) { + let traktResult = false; + if (item.type === 'movie') { + traktResult = await traktService.removeMovieFromHistory(item.id); + } else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) { + traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode); } else { - logger.log(`â„šī¸ [ContinueWatching] Skipping Trakt removal - not authenticated`); + traktResult = await traktService.removeShowFromHistory(item.id); } - - // Track this item as recently removed to prevent immediate re-addition - const itemKey = `${item.type}:${item.id}`; - recentlyRemovedRef.current.add(itemKey); - - // Persist the removed state for long-term tracking - await storageService.addContinueWatchingRemoved(item.id, item.type); - - // Clear from recently removed after the ignore duration - setTimeout(() => { - recentlyRemovedRef.current.delete(itemKey); - }, REMOVAL_IGNORE_DURATION); - - // Update the list by filtering out the deleted item - setContinueWatchingItems(prev => { - const newList = prev.filter(i => i.id !== item.id); - return newList; - }); - - } catch (error) { - // Continue even if removal fails - } finally { - setDeletingItemId(null); } + const itemKey = `${item.type}:${item.id}`; + recentlyRemovedRef.current.add(itemKey); + await storageService.addContinueWatchingRemoved(item.id, item.type); + setTimeout(() => { + recentlyRemovedRef.current.delete(itemKey); + }, REMOVAL_IGNORE_DURATION); + setContinueWatchingItems(prev => prev.filter(i => i.id !== item.id)); + } catch (error) { + // Continue even if removal fails + } finally { + setDeletingItemId(null); } - } - ] - ); - }, []); + }, + }, + ]); + setAlertVisible(true); + }, [currentTheme.colors.error]); // Memoized render function for continue watching items const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( @@ -730,6 +707,14 @@ const ContinueWatchingSection = React.forwardRef((props, re onEndReached={() => {}} removeClippedSubviews={true} /> + + setAlertVisible(false)} + /> ); }); diff --git a/src/components/metadata/MoreLikeThisSection.tsx b/src/components/metadata/MoreLikeThisSection.tsx index 727cb5c..7830f1c 100644 --- a/src/components/metadata/MoreLikeThisSection.tsx +++ b/src/components/metadata/MoreLikeThisSection.tsx @@ -7,16 +7,16 @@ import { TouchableOpacity, ActivityIndicator, Dimensions, - Alert, } from 'react-native'; import { Image } from 'expo-image'; import { useNavigation, StackActions } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; -import { StreamingContent } from '../../types/metadata'; +import { StreamingContent } from '../../services/catalogService'; import { useTheme } from '../../contexts/ThemeContext'; import { TMDBService } from '../../services/tmdbService'; import { catalogService } from '../../services/catalogService'; +import CustomAlert from '../../components/CustomAlert'; const { width } = Dimensions.get('window'); @@ -59,6 +59,11 @@ export const MoreLikeThisSection: React.FC = ({ const { currentTheme } = useTheme(); const navigation = useNavigation>(); + const [alertVisible, setAlertVisible] = React.useState(false); + const [alertTitle, setAlertTitle] = React.useState(''); + const [alertMessage, setAlertMessage] = React.useState(''); + const [alertActions, setAlertActions] = React.useState([]); + const handleItemPress = async (item: StreamingContent) => { try { // Extract TMDB ID from the tmdb:123456 format @@ -80,11 +85,10 @@ export const MoreLikeThisSection: React.FC = ({ } } catch (error) { if (__DEV__) console.error('Error navigating to recommendation:', error); - Alert.alert( - 'Error', - 'Unable to load this content. Please try again later.', - [{ text: 'OK' }] - ); + setAlertTitle('Error'); + setAlertMessage('Unable to load this content. Please try again later.'); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); } }; @@ -128,6 +132,13 @@ export const MoreLikeThisSection: React.FC = ({ showsHorizontalScrollIndicator={false} contentContainerStyle={styles.listContentContainer} /> + setAlertVisible(false)} + /> ); }; @@ -169,4 +180,4 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/src/hooks/useUpdatePopup.ts b/src/hooks/useUpdatePopup.ts index eb6e191..9885190 100644 --- a/src/hooks/useUpdatePopup.ts +++ b/src/hooks/useUpdatePopup.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { Alert, Platform } from 'react-native'; +import { Platform } from 'react-native'; import { toast, ToastPosition } from '@backpackapp-io/react-native-toast'; import UpdateService, { UpdateInfo } from '../services/updateService'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -78,19 +78,19 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => { // The app will automatically reload with the new version console.log('Update installed successfully'); } else { - Alert.alert( - 'Update Failed', - 'Unable to install the update. Please try again later or check your internet connection.' - ); + toast('Unable to install the update. Please try again later or check your internet connection.', { + duration: 3000, + position: ToastPosition.TOP, + }); // Show popup again after failed installation setShowUpdatePopup(true); } } catch (error) { if (__DEV__) console.error('Error installing update:', error); - Alert.alert( - 'Update Error', - 'An error occurred while installing the update. Please try again later.' - ); + toast('An error occurred while installing the update. Please try again later.', { + duration: 3000, + position: ToastPosition.TOP, + }); // Show popup again after error setShowUpdatePopup(true); } finally { diff --git a/src/screens/AIChatScreen.tsx b/src/screens/AIChatScreen.tsx index 2c28ccd..584c629 100644 --- a/src/screens/AIChatScreen.tsx +++ b/src/screens/AIChatScreen.tsx @@ -11,9 +11,10 @@ import { Platform, Dimensions, ActivityIndicator, - Alert, Keyboard, } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; +// Removed duplicate AIChatScreen definition and alert state at the top. The correct component is defined after SuggestionChip. import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; @@ -297,6 +298,34 @@ const SuggestionChip: React.FC = React.memo(({ text, onPres }, (prev, next) => prev.text === next.text && prev.onPress === next.onPress); const AIChatScreen: React.FC = () => { + // CustomAlert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([ + { label: 'OK', onPress: () => setAlertVisible(false) }, + ]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress?: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + if (actions && actions.length > 0) { + setAlertActions( + actions.map(a => ({ + label: a.label, + style: a.style, + onPress: () => { a.onPress?.(); setAlertVisible(false); }, + })) + ); + } else { + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + } + setAlertVisible(true); + }; const route = useRoute(); const navigation = useNavigation(); const { currentTheme } = useTheme(); @@ -438,9 +467,17 @@ const AIChatScreen: React.FC = () => { } } catch (error) { if (__DEV__) console.error('Error loading context:', error); - Alert.alert('Error', 'Failed to load content details for AI chat'); + openAlert('Error', 'Failed to load content details for AI chat'); } finally { setIsLoadingContext(false); + {/* CustomAlert at root */} + setAlertVisible(false)} + actions={alertActions} + /> } }; @@ -786,6 +823,13 @@ const AIChatScreen: React.FC = () => { + setAlertVisible(false)} + actions={alertActions} + /> ); }; diff --git a/src/screens/AISettingsScreen.tsx b/src/screens/AISettingsScreen.tsx index e6c555c..5a4a789 100644 --- a/src/screens/AISettingsScreen.tsx +++ b/src/screens/AISettingsScreen.tsx @@ -8,12 +8,12 @@ import { StatusBar, TextInput, TouchableOpacity, - Alert, Linking, Platform, Dimensions, Switch, } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -26,6 +26,34 @@ const { width } = Dimensions.get('window'); const isTablet = width >= 768; const AISettingsScreen: React.FC = () => { + // CustomAlert state (must be inside the component) + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([ + { label: 'OK', onPress: () => setAlertVisible(false) }, + ]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress?: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + if (actions && actions.length > 0) { + setAlertActions( + actions.map(a => ({ + label: a.label, + style: a.style, + onPress: () => { a.onPress?.(); }, + })) + ); + } else { + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + } + setAlertVisible(true); + }; const navigation = useNavigation(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); @@ -64,12 +92,12 @@ const AISettingsScreen: React.FC = () => { const handleSaveApiKey = async () => { if (!apiKey.trim()) { - Alert.alert('Error', 'Please enter a valid API key'); + openAlert('Error', 'Please enter a valid API key'); return; } if (!apiKey.startsWith('sk-or-')) { - Alert.alert('Error', 'OpenRouter API keys should start with "sk-or-"'); + openAlert('Error', 'OpenRouter API keys should start with "sk-or-"'); return; } @@ -77,9 +105,9 @@ const AISettingsScreen: React.FC = () => { try { await AsyncStorage.setItem('openrouter_api_key', apiKey.trim()); setIsKeySet(true); - Alert.alert('Success', 'OpenRouter API key saved successfully!'); + openAlert('Success', 'OpenRouter API key saved successfully!'); } catch (error) { - Alert.alert('Error', 'Failed to save API key'); + openAlert('Error', 'Failed to save API key'); if (__DEV__) console.error('Error saving OpenRouter API key:', error); } finally { setLoading(false); @@ -87,22 +115,21 @@ const AISettingsScreen: React.FC = () => { }; const handleRemoveApiKey = () => { - Alert.alert( + openAlert( 'Remove API Key', 'Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.', [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Remove', - style: 'destructive', + label: 'Remove', onPress: async () => { try { await AsyncStorage.removeItem('openrouter_api_key'); setApiKey(''); setIsKeySet(false); - Alert.alert('Success', 'API key removed successfully'); + openAlert('Success', 'API key removed successfully'); } catch (error) { - Alert.alert('Error', 'Failed to remove API key'); + openAlert('Error', 'Failed to remove API key'); } } } @@ -115,7 +142,7 @@ const AISettingsScreen: React.FC = () => { }; return ( - + {/* Header */} @@ -344,6 +371,13 @@ const AISettingsScreen: React.FC = () => { + setAlertVisible(false)} + actions={alertActions} + /> ); }; diff --git a/src/screens/AccountManageScreen.tsx b/src/screens/AccountManageScreen.tsx index 1cf2ad0..f523253 100644 --- a/src/screens/AccountManageScreen.tsx +++ b/src/screens/AccountManageScreen.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Alert, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native'; import { Image } from 'expo-image'; import { useNavigation } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -8,6 +8,7 @@ import { useAccount } from '../contexts/AccountContext'; import { useTheme } from '../contexts/ThemeContext'; import { LinearGradient } from 'expo-linear-gradient'; import * as Haptics from 'expo-haptics'; +import CustomAlert from '../components/CustomAlert'; const AccountManageScreen: React.FC = () => { const navigation = useNavigation(); @@ -34,6 +35,10 @@ const AccountManageScreen: React.FC = () => { const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || ''); const [saving, setSaving] = useState(false); const [avatarError, setAvatarError] = useState(false); + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); useEffect(() => { // Reset image error state when URL changes @@ -45,31 +50,32 @@ const AccountManageScreen: React.FC = () => { setSaving(true); const err = await updateProfile({ displayName: displayName.trim() || undefined, avatarUrl: avatarUrl.trim() || undefined }); if (err) { - Alert.alert('Error', err); + setAlertTitle('Error'); + setAlertMessage(err); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); } setSaving(false); }; const handleSignOut = () => { - Alert.alert( - 'Sign out', - 'Are you sure you want to sign out?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Sign out', - style: 'destructive', - onPress: async () => { - try { - await signOut(); - // Navigate back to root after sign out - // @ts-ignore - navigation.goBack(); - } catch (_) {} - }, + setAlertTitle('Sign out'); + setAlertMessage('Are you sure you want to sign out?'); + setAlertActions([ + { label: 'Cancel', onPress: () => {} }, + { + label: 'Sign out', + onPress: async () => { + try { + await signOut(); + // @ts-ignore + navigation.goBack(); + } catch (_) {} }, - ] - ); + style: { opacity: 1 }, + }, + ]); + setAlertVisible(true); }; return ( @@ -207,6 +213,13 @@ const AccountManageScreen: React.FC = () => { Sign out + setAlertVisible(false)} + /> ); }; diff --git a/src/screens/AddonsScreen.tsx b/src/screens/AddonsScreen.tsx index 2d6d192..1bc8b64 100644 --- a/src/screens/AddonsScreen.tsx +++ b/src/screens/AddonsScreen.tsx @@ -7,7 +7,6 @@ import { TextInput, TouchableOpacity, ActivityIndicator, - Alert, SafeAreaView, StatusBar, Modal, @@ -30,6 +29,7 @@ import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { BlurView as ExpoBlurView } from 'expo-blur'; +import CustomAlert from '../components/CustomAlert'; // Removed community blur and expo-constants for Android overlay import axios from 'axios'; import { useTheme } from '../contexts/ThemeContext'; @@ -597,6 +597,11 @@ const AddonsScreen = () => { const [installing, setInstalling] = useState(false); const [catalogCount, setCatalogCount] = useState(0); // Add state for reorder mode + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); const [reorderMode, setReorderMode] = useState(false); // Use ThemeContext const { currentTheme } = useTheme(); @@ -662,8 +667,11 @@ const AddonsScreen = () => { setCatalogCount(totalCatalogs); } } catch (error) { - logger.error('Failed to load addons:', error); - Alert.alert('Error', 'Failed to load addons'); + logger.error('Failed to load addons:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to load addons'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setLoading(false); } @@ -683,10 +691,9 @@ const AddonsScreen = () => { setCommunityAddons(validAddons); } catch (error) { - logger.error('Failed to load community addons:', error); - setCommunityError('Failed to load community addons. Please try again later.'); - // Set empty array on error since Cinemeta is pre-installed - setCommunityAddons([]); + logger.error('Failed to load community addons:', error); + setCommunityError('Failed to load community addons. Please try again later.'); + setCommunityAddons([]); } finally { setCommunityLoading(false); } @@ -695,7 +702,10 @@ const AddonsScreen = () => { const handleAddAddon = async (url?: string) => { const urlToInstall = url || addonUrl; if (!urlToInstall) { - Alert.alert('Error', 'Please enter an addon URL or select a community addon'); + setAlertTitle('Error'); + setAlertMessage('Please enter an addon URL or select a community addon'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); return; } @@ -706,8 +716,11 @@ const AddonsScreen = () => { setAddonUrl(urlToInstall); setShowConfirmModal(true); } catch (error) { - logger.error('Failed to fetch addon details:', error); - Alert.alert('Error', `Failed to fetch addon details from ${urlToInstall}`); + logger.error('Failed to fetch addon details:', error); + setAlertTitle('Error'); + setAlertMessage(`Failed to fetch addon details from ${urlToInstall}`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setInstalling(false); } @@ -723,10 +736,16 @@ const AddonsScreen = () => { setShowConfirmModal(false); setAddonDetails(null); loadAddons(); - Alert.alert('Success', 'Addon installed successfully'); + setAlertTitle('Success'); + setAlertMessage('Addon installed successfully'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } catch (error) { - logger.error('Failed to install addon:', error); - Alert.alert('Error', 'Failed to install addon'); + logger.error('Failed to install addon:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to install addon'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setInstalling(false); } @@ -754,31 +773,26 @@ const AddonsScreen = () => { const handleRemoveAddon = (addon: ExtendedManifest) => { // Check if this is a pre-installed addon if (stremioService.isPreInstalledAddon(addon.id)) { - Alert.alert( - 'Cannot Remove Addon', - `${addon.name} is a pre-installed addon and cannot be removed.`, - [{ text: 'OK', style: 'default' }] - ); + setAlertTitle('Cannot Remove Addon'); + setAlertMessage(`${addon.name} is a pre-installed addon and cannot be removed.`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); return; } - - Alert.alert( - 'Uninstall Addon', - `Are you sure you want to uninstall ${addon.name}?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Uninstall', - style: 'destructive', - onPress: () => { - stremioService.removeAddon(addon.id); - - // Remove from addons list - setAddons(prev => prev.filter(a => a.id !== addon.id)); - }, + setAlertTitle('Uninstall Addon'); + setAlertMessage(`Are you sure you want to uninstall ${addon.name}?`); + setAlertActions([ + { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } }, + { + label: 'Uninstall', + onPress: () => { + stremioService.removeAddon(addon.id); + setAddons(prev => prev.filter(a => a.id !== addon.id)); }, - ] - ); + style: { color: colors.error } + }, + ]); + setAlertVisible(true); }; // Add function to handle configuration @@ -876,11 +890,10 @@ const AddonsScreen = () => { // If we couldn't determine a config URL, show an error if (!configUrl) { logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`); - Alert.alert( - 'Configuration Unavailable', - 'Could not determine configuration URL for this addon.', - [{ text: 'OK' }] - ); + setAlertTitle('Configuration Unavailable'); + setAlertMessage('Could not determine configuration URL for this addon.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); return; } @@ -893,15 +906,17 @@ const AddonsScreen = () => { Linking.openURL(configUrl); } else { logger.error(`URL cannot be opened: ${configUrl}`); - Alert.alert( - 'Cannot Open Configuration', - `The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`, - [{ text: 'OK' }] - ); + setAlertTitle('Cannot Open Configuration'); + setAlertMessage(`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } }).catch(err => { logger.error(`Error checking if URL can be opened: ${configUrl}`, err); - Alert.alert('Error', 'Could not open configuration page.'); + setAlertTitle('Error'); + setAlertMessage('Could not open configuration page.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); }); }; @@ -1482,7 +1497,15 @@ const AddonsScreen = () => { - + {/* Custom Alert Modal */} + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/CastMoviesScreen.tsx b/src/screens/CastMoviesScreen.tsx index e74862b..80e3032 100644 --- a/src/screens/CastMoviesScreen.tsx +++ b/src/screens/CastMoviesScreen.tsx @@ -7,7 +7,6 @@ import { ActivityIndicator, Dimensions, Platform, - Alert, FlatList, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -35,6 +34,7 @@ import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { StackActions } from '@react-navigation/native'; +import CustomAlert from '../components/CustomAlert'; const { width, height } = Dimensions.get('window'); const isTablet = width >= 768; @@ -71,6 +71,10 @@ const CastMoviesScreen: React.FC = () => { const scrollY = useSharedValue(0); const [displayLimit, setDisplayLimit] = useState(30); // Start with fewer items for performance const [isLoadingMore, setIsLoadingMore] = useState(false); + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); useEffect(() => { if (castMember) { @@ -253,7 +257,7 @@ const CastMoviesScreen: React.FC = () => { if (__DEV__) console.warn('Stremio ID is null/undefined for movie:', movie.title); throw new Error('Could not find Stremio ID'); } - } catch (error: any) { + } catch (error: any) { if (__DEV__) { console.error('=== Error in handleMoviePress ==='); console.error('Movie:', movie.title); @@ -261,12 +265,10 @@ const CastMoviesScreen: React.FC = () => { console.error('Error message:', error.message); console.error('Error stack:', error.stack); } - - Alert.alert( - 'Error', - `Unable to load "${movie.title}". Please try again later.`, - [{ text: 'OK' }] - ); + setAlertTitle('Error'); + setAlertMessage(`Unable to load "${movie.title}". Please try again later.`); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); } }; @@ -810,8 +812,18 @@ const CastMoviesScreen: React.FC = () => { } /> )} + + {/* Inject CustomAlert component to display errors */} + setAlertVisible(false)} + /> ); }; export default CastMoviesScreen; + diff --git a/src/screens/CatalogSettingsScreen.tsx b/src/screens/CatalogSettingsScreen.tsx index 3461e14..28d8052 100644 --- a/src/screens/CatalogSettingsScreen.tsx +++ b/src/screens/CatalogSettingsScreen.tsx @@ -14,7 +14,6 @@ import { TextInput, Pressable, Button, - Alert, } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useNavigation } from '@react-navigation/native'; @@ -25,6 +24,7 @@ import { useCatalogContext } from '../contexts/CatalogContext'; import { logger } from '../utils/logger'; import { clearCustomNameCache } from '../utils/catalogNameUtils'; import { BlurView } from 'expo-blur'; +import CustomAlert from '../components/CustomAlert'; interface CatalogSetting { addonId: string; @@ -264,6 +264,10 @@ const CatalogSettingsScreen = () => { const [isRenameModalVisible, setIsRenameModalVisible] = useState(false); const [catalogToRename, setCatalogToRename] = useState(null); const [currentRenameValue, setCurrentRenameValue] = useState(''); + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); // Load saved settings and available catalogs const loadSettings = useCallback(async () => { @@ -465,7 +469,10 @@ const CatalogSettingsScreen = () => { } catch (error) { logger.error('Failed to save custom catalog name:', error); - Alert.alert('Error', 'Could not save the custom name.'); // Inform user + setAlertTitle('Error'); + setAlertMessage('Could not save the custom name.'); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); } finally { setIsRenameModalVisible(false); setCatalogToRename(null); @@ -688,8 +695,15 @@ const CatalogSettingsScreen = () => { )} + setAlertVisible(false)} + /> ); }; -export default CatalogSettingsScreen; \ No newline at end of file +export default CatalogSettingsScreen; diff --git a/src/screens/HeroCatalogsScreen.tsx b/src/screens/HeroCatalogsScreen.tsx index 79514c8..bf39e11 100644 --- a/src/screens/HeroCatalogsScreen.tsx +++ b/src/screens/HeroCatalogsScreen.tsx @@ -11,9 +11,9 @@ import { Platform, useColorScheme, ActivityIndicator, - Alert, Animated } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import { useSettings, settingsEmitter } from '../hooks/useSettings'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -35,6 +35,11 @@ const HeroCatalogsScreen: React.FC = () => { const systemColorScheme = useColorScheme(); const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode; const navigation = useNavigation(); + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); const [loading, setLoading] = useState(true); const [catalogs, setCatalogs] = useState([]); const [selectedCatalogs, setSelectedCatalogs] = useState(settings.selectedHeroCatalogs || []); @@ -120,7 +125,10 @@ const HeroCatalogsScreen: React.FC = () => { setCatalogs(catalogItems); } catch (error) { if (__DEV__) console.error('Failed to load catalogs:', error); - Alert.alert('Error', 'Failed to load catalogs'); + setAlertTitle('Error'); + setAlertMessage('Failed to load catalogs'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setLoading(false); } @@ -276,7 +284,14 @@ const HeroCatalogsScreen: React.FC = () => { )} - + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/LogoSourceSettings.tsx b/src/screens/LogoSourceSettings.tsx index 12daf9b..2f5b5d0 100644 --- a/src/screens/LogoSourceSettings.tsx +++ b/src/screens/LogoSourceSettings.tsx @@ -8,7 +8,6 @@ import { Switch, SafeAreaView, Image, - Alert, StatusBar, Platform, ActivityIndicator, @@ -21,6 +20,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { TMDBService } from '../services/tmdbService'; import { logger } from '../utils/logger'; import { useTheme } from '../contexts/ThemeContext'; +import CustomAlert from '../components/CustomAlert'; // TMDB API key - since the default key might be private in the service, we'll use our own const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; @@ -358,7 +358,36 @@ const LogoSourceSettings = () => { const { currentTheme } = useTheme(); const colors = currentTheme.colors; const styles = createStyles(colors); - + + // CustomAlert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([ + { label: 'OK', onPress: () => setAlertVisible(false) }, + ]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress?: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + if (actions && actions.length > 0) { + setAlertActions( + actions.map(a => ({ + label: a.label, + style: a.style, + onPress: () => { a.onPress?.(); }, + })) + ); + } else { + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + } + setAlertVisible(true); + }; + // Get current preference const [logoSource, setLogoSource] = useState<'metahub' | 'tmdb'>( settings.logoSourcePreference || 'metahub' @@ -560,10 +589,9 @@ const LogoSourceSettings = () => { } // Show confirmation alert - Alert.alert( + openAlert( 'Settings Updated', - `Logo and background source preference set to ${source === 'metahub' ? 'Metahub' : 'TMDB'}. Changes will apply when you navigate to content.`, - [{ text: 'OK' }] + `Logo and background source preference set to ${source === 'metahub' ? 'Metahub' : 'TMDB'}. Changes will apply when you navigate to content.` ); }; @@ -599,19 +627,17 @@ const LogoSourceSettings = () => { await AsyncStorage.removeItem('_last_logos_'); // Show confirmation toast or feedback - Alert.alert( + openAlert( 'TMDB Language Updated', - `TMDB logo language preference set to ${languageCode.toUpperCase()}. Changes will apply when you navigate to content.`, - [{ text: 'OK' }] + `TMDB logo language preference set to ${languageCode.toUpperCase()}. Changes will apply when you navigate to content.` ); } catch (e) { logger.error(`[LogoSourceSettings] Error in saveLanguagePreference:`, e); // Show error notification - Alert.alert( + openAlert( 'Error Saving Preference', - 'There was a problem saving your language preference. Please try again.', - [{ text: 'OK' }] + 'There was a problem saving your language preference. Please try again.' ); } }; @@ -692,7 +718,6 @@ const LogoSourceSettings = () => { return ( - {/* Header */} { Settings - {/* Empty for now, but ready for future actions */} - Logo Source - { + setAlertVisible(false)} + actions={alertActions} + /> ); }; diff --git a/src/screens/MDBListSettingsScreen.tsx b/src/screens/MDBListSettingsScreen.tsx index 6634d1a..019044b 100644 --- a/src/screens/MDBListSettingsScreen.tsx +++ b/src/screens/MDBListSettingsScreen.tsx @@ -8,7 +8,6 @@ import { SafeAreaView, StatusBar, Platform, - Alert, ActivityIndicator, Linking, ScrollView, @@ -16,6 +15,7 @@ import { Clipboard, Switch, } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import { useNavigation } from '@react-navigation/native'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -361,6 +361,11 @@ const MDBListSettingsScreen = () => { const colors = currentTheme.colors; const styles = createStyles(colors); + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); const [apiKey, setApiKey] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isKeySet, setIsKeySet] = useState(false); @@ -492,34 +497,32 @@ const MDBListSettingsScreen = () => { const clearApiKey = async () => { logger.log('[MDBListSettingsScreen] Clear API key requested'); - Alert.alert( - 'Clear API Key', - 'Are you sure you want to remove the saved API key?', - [ - { - text: 'Cancel', - style: 'cancel', - onPress: () => logger.log('[MDBListSettingsScreen] Clear API key cancelled') - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - logger.log('[MDBListSettingsScreen] Proceeding with API key clear'); - try { - await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY); - setApiKey(''); - setIsKeySet(false); - setTestResult(null); - logger.log('[MDBListSettingsScreen] API key cleared successfully'); - } catch (error) { - logger.error('[MDBListSettingsScreen] Failed to clear API key:', error); - Alert.alert('Error', 'Failed to clear API key'); - } + setAlertTitle('Clear API Key'); + setAlertMessage('Are you sure you want to remove the saved API key?'); + setAlertActions([ + { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } }, + { + label: 'Clear', + onPress: async () => { + logger.log('[MDBListSettingsScreen] Proceeding with API key clear'); + try { + await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY); + setApiKey(''); + setIsKeySet(false); + setTestResult(null); + logger.log('[MDBListSettingsScreen] API key cleared successfully'); + } catch (error) { + logger.error('[MDBListSettingsScreen] Failed to clear API key:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to clear API key'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } - } - ] - ); + }, + style: { color: colors.error } + }, + ]); + setAlertVisible(true); }; const pasteFromClipboard = async () => { @@ -823,7 +826,14 @@ const MDBListSettingsScreen = () => { - + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/NotificationSettingsScreen.tsx b/src/screens/NotificationSettingsScreen.tsx index 2708c55..15568cc 100644 --- a/src/screens/NotificationSettingsScreen.tsx +++ b/src/screens/NotificationSettingsScreen.tsx @@ -6,11 +6,11 @@ import { ScrollView, Switch, TouchableOpacity, - Alert, SafeAreaView, StatusBar, Platform, } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; import { notificationService, NotificationSettings } from '../services/notificationService'; @@ -36,6 +36,11 @@ const NotificationSettingsScreen = () => { const [isSyncing, setIsSyncing] = useState(false); const [notificationStats, setNotificationStats] = useState({ total: 0, upcoming: 0, thisWeek: 0 }); + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); // Load settings and stats on mount useEffect(() => { const loadSettings = async () => { @@ -104,7 +109,10 @@ const NotificationSettingsScreen = () => { setSettings(updatedSettings); } catch (error) { logger.error('Error updating notification settings:', error); - Alert.alert('Error', 'Failed to update notification settings'); + setAlertTitle('Error'); + setAlertMessage('Failed to update notification settings'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } }; @@ -114,33 +122,34 @@ const NotificationSettingsScreen = () => { }; const resetAllNotifications = async () => { - Alert.alert( - 'Reset Notifications', - 'This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Reset', - style: 'destructive', - onPress: async () => { - try { - // Cancel all notifications for all series, but do not remove from saved - const scheduledNotifications = notificationService.getScheduledNotifications?.() || []; - for (const notification of scheduledNotifications) { - await notificationService.cancelNotification(notification.id); - } - Alert.alert('Success', 'All notifications have been reset'); - } catch (error) { - logger.error('Error resetting notifications:', error); - Alert.alert('Error', 'Failed to reset notifications'); + setAlertTitle('Reset Notifications'); + setAlertMessage('This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?'); + setAlertActions([ + { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: currentTheme.colors.mediumGray } }, + { + label: 'Reset', + onPress: async () => { + try { + const scheduledNotifications = notificationService.getScheduledNotifications?.() || []; + for (const notification of scheduledNotifications) { + await notificationService.cancelNotification(notification.id); } - }, + setAlertTitle('Success'); + setAlertMessage('All notifications have been reset'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + } catch (error) { + logger.error('Error resetting notifications:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to reset notifications'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + } }, - ] - ); + style: { color: currentTheme.colors.error } + }, + ]); + setAlertVisible(true); }; const handleSyncNotifications = async () => { @@ -154,13 +163,16 @@ const NotificationSettingsScreen = () => { const stats = notificationService.getNotificationStats(); setNotificationStats(stats); - Alert.alert( - 'Sync Complete', - `Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes` - ); + setAlertTitle('Sync Complete'); + setAlertMessage(`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } catch (error) { logger.error('Error syncing notifications:', error); - Alert.alert('Error', 'Failed to sync notifications. Please try again.'); + setAlertTitle('Error'); + setAlertMessage('Failed to sync notifications. Please try again.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setIsSyncing(false); } @@ -212,13 +224,22 @@ const NotificationSettingsScreen = () => { if (notificationId) { setTestNotificationId(notificationId); setCountdown(0); // No countdown for instant notification - Alert.alert('Success', 'Test notification scheduled to fire instantly'); + setAlertTitle('Success'); + setAlertMessage('Test notification scheduled to fire instantly'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } else { - Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.'); + setAlertTitle('Error'); + setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } } catch (error) { logger.error('Error scheduling test notification:', error); - Alert.alert('Error', 'Failed to schedule test notification'); + setAlertTitle('Error'); + setAlertMessage('Failed to schedule test notification'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } }; @@ -475,7 +496,14 @@ const NotificationSettingsScreen = () => { )} - + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index 7e3b134..a1f92eb 100644 --- a/src/screens/PluginsScreen.tsx +++ b/src/screens/PluginsScreen.tsx @@ -4,7 +4,6 @@ import { Text, StyleSheet, TouchableOpacity, - Alert, Switch, TextInput, ScrollView, @@ -16,6 +15,7 @@ import { Dimensions, Animated, } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import { Image } from 'expo-image'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; @@ -826,6 +826,23 @@ const PluginsScreen: React.FC = () => { const colors = currentTheme.colors; const styles = createStyles(colors); + // CustomAlert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); + }; + // Core state const [repositoryUrl, setRepositoryUrl] = useState(settings.scraperRepositoryUrl); const [installedScrapers, setInstalledScrapers] = useState([]); @@ -915,10 +932,10 @@ const PluginsScreen: React.FC = () => { ); await Promise.all(promises); await loadScrapers(); - Alert.alert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredScrapers.length} scrapers`); + openAlert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredScrapers.length} scrapers`); } catch (error) { logger.error('[ScraperSettings] Failed to bulk toggle:', error); - Alert.alert('Error', 'Failed to update scrapers'); + openAlert('Error', 'Failed to update scrapers'); } finally { setIsRefreshing(false); } @@ -930,14 +947,14 @@ const PluginsScreen: React.FC = () => { const handleAddRepository = async () => { if (!newRepositoryUrl.trim()) { - Alert.alert('Error', 'Please enter a valid repository URL'); + openAlert('Error', 'Please enter a valid repository URL'); return; } // Validate URL format const url = newRepositoryUrl.trim(); if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { - Alert.alert( + openAlert( 'Invalid URL Format', 'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nor include manifest.json:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch/manifest.json\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master' ); @@ -956,7 +973,7 @@ const PluginsScreen: React.FC = () => { // Additional validation for normalized URL if (!normalizedUrl.endsWith('/refs/heads/') && !normalizedUrl.includes('/refs/heads/')) { - Alert.alert( + openAlert( 'Invalid Repository Structure', 'The URL should point to a GitHub repository branch.\n\nExpected format:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch' ); @@ -981,10 +998,10 @@ const PluginsScreen: React.FC = () => { setNewRepositoryUrl(''); setShowAddRepositoryModal(false); - Alert.alert('Success', 'Repository added and refreshed successfully'); + openAlert('Success', 'Repository added and refreshed successfully'); } catch (error) { logger.error('[PluginsScreen] Failed to add repository:', error); - Alert.alert('Error', 'Failed to add repository'); + openAlert('Error', 'Failed to add repository'); } finally { setIsLoading(false); } @@ -996,10 +1013,10 @@ const PluginsScreen: React.FC = () => { await localScraperService.setCurrentRepository(repoId); await loadRepositories(); await loadScrapers(); - Alert.alert('Success', 'Repository switched successfully'); + openAlert('Success', 'Repository switched successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to switch repository:', error); - Alert.alert('Error', 'Failed to switch repository'); + openAlert('Error', 'Failed to switch repository'); } finally { setSwitchingRepository(null); } @@ -1017,14 +1034,13 @@ const PluginsScreen: React.FC = () => { ? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no scrapers available until you add a new repository.` : `Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`; - Alert.alert( + openAlert( alertTitle, alertMessage, [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Remove', - style: 'destructive', + label: 'Remove', onPress: async () => { try { await localScraperService.removeRepository(repoId); @@ -1033,10 +1049,10 @@ const PluginsScreen: React.FC = () => { const successMessage = isLastRepository ? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.' : 'Repository removed successfully'; - Alert.alert('Success', successMessage); + openAlert('Success', successMessage); } catch (error) { logger.error('[ScraperSettings] Failed to remove repository:', error); - Alert.alert('Error', error instanceof Error ? error.message : 'Failed to remove repository'); + openAlert('Error', error instanceof Error ? error.message : 'Failed to remove repository'); } }, }, @@ -1102,14 +1118,14 @@ const PluginsScreen: React.FC = () => { const handleSaveRepository = async () => { if (!repositoryUrl.trim()) { - Alert.alert('Error', 'Please enter a valid repository URL'); + openAlert('Error', 'Please enter a valid repository URL'); return; } // Validate URL format const url = repositoryUrl.trim(); if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) { - Alert.alert( + openAlert( 'Invalid URL Format', 'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master' ); @@ -1121,10 +1137,10 @@ const PluginsScreen: React.FC = () => { await localScraperService.setRepositoryUrl(url); await updateSetting('scraperRepositoryUrl', url); setHasRepository(true); - Alert.alert('Success', 'Repository URL saved successfully'); + openAlert('Success', 'Repository URL saved successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to save repository:', error); - Alert.alert('Error', 'Failed to save repository URL'); + openAlert('Error', 'Failed to save repository URL'); } finally { setIsLoading(false); } @@ -1132,7 +1148,7 @@ const PluginsScreen: React.FC = () => { const handleRefreshRepository = async () => { if (!repositoryUrl.trim()) { - Alert.alert('Error', 'Please set a repository URL first'); + openAlert('Error', 'Please set a repository URL first'); return; } @@ -1146,11 +1162,11 @@ const PluginsScreen: React.FC = () => { // Load fresh scrapers from the updated repository await loadScrapers(); - Alert.alert('Success', 'Repository refreshed successfully with latest files'); + openAlert('Success', 'Repository refreshed successfully with latest files'); } catch (error) { logger.error('[PluginsScreen] Failed to refresh repository:', error); const errorMessage = error instanceof Error ? error.message : String(error); - Alert.alert( + openAlert( 'Repository Error', `Failed to refresh repository: ${errorMessage}\n\nPlease ensure your URL is correct and follows this format:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch` ); @@ -1178,28 +1194,27 @@ const PluginsScreen: React.FC = () => { await loadScrapers(); } catch (error) { logger.error('[ScraperSettings] Failed to toggle scraper:', error); - Alert.alert('Error', 'Failed to update scraper status'); + openAlert('Error', 'Failed to update scraper status'); setIsRefreshing(false); } }; const handleClearScrapers = () => { - Alert.alert( + openAlert( 'Clear All Scrapers', 'Are you sure you want to remove all installed scrapers? This action cannot be undone.', [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Clear', - style: 'destructive', + label: 'Clear', onPress: async () => { try { await localScraperService.clearScrapers(); await loadScrapers(); - Alert.alert('Success', 'All scrapers have been removed'); + openAlert('Success', 'All scrapers have been removed'); } catch (error) { logger.error('[ScraperSettings] Failed to clear scrapers:', error); - Alert.alert('Error', 'Failed to clear scrapers'); + openAlert('Error', 'Failed to clear scrapers'); } }, }, @@ -1208,14 +1223,13 @@ const PluginsScreen: React.FC = () => { }; const handleClearCache = () => { - Alert.alert( + openAlert( 'Clear Repository Cache', 'This will remove the saved repository URL and clear all cached scraper data. You will need to re-enter your repository URL.', [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Clear Cache', - style: 'destructive', + label: 'Clear Cache', onPress: async () => { try { await localScraperService.clearScrapers(); @@ -1224,10 +1238,10 @@ const PluginsScreen: React.FC = () => { setRepositoryUrl(''); setHasRepository(false); await loadScrapers(); - Alert.alert('Success', 'Repository cache cleared successfully'); + openAlert('Success', 'Repository cache cleared successfully'); } catch (error) { logger.error('[ScraperSettings] Failed to clear cache:', error); - Alert.alert('Error', 'Failed to clear repository cache'); + openAlert('Error', 'Failed to clear repository cache'); } }, }, @@ -1445,10 +1459,10 @@ const PluginsScreen: React.FC = () => { const tapframeInfo = localScraperService.getTapframeRepositoryInfo(); const repoId = await localScraperService.addRepository(tapframeInfo); await loadRepositories(); - Alert.alert('Success', 'Official repository added successfully!'); + openAlert('Success', 'Official repository added successfully!'); } catch (error) { logger.error('[PluginsScreen] Failed to add tapframe repository:', error); - Alert.alert('Error', 'Failed to add official repository'); + openAlert('Error', 'Failed to add official repository'); } finally { setIsLoading(false); } @@ -1662,7 +1676,7 @@ const PluginsScreen: React.FC = () => { style={[styles.button, styles.primaryButton]} onPress={async () => { await localScraperService.setScraperSettings('showboxog', { cookie: showboxCookie, region: showboxRegion }); - Alert.alert('Saved', 'ShowBox settings updated'); + openAlert('Saved', 'ShowBox settings updated'); }} > Save @@ -1924,6 +1938,13 @@ const PluginsScreen: React.FC = () => { + setAlertVisible(false)} + /> ); }; diff --git a/src/screens/ProfilesScreen.tsx b/src/screens/ProfilesScreen.tsx index 008f3b4..8d12d12 100644 --- a/src/screens/ProfilesScreen.tsx +++ b/src/screens/ProfilesScreen.tsx @@ -5,7 +5,6 @@ import { StyleSheet, TouchableOpacity, FlatList, - Alert, StatusBar, Platform, SafeAreaView, @@ -17,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; import { useTraktContext } from '../contexts/TraktContext'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import CustomAlert from '../components/CustomAlert'; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const PROFILE_STORAGE_KEY = 'user_profiles'; @@ -39,6 +39,23 @@ const ProfilesScreen: React.FC = () => { const [newProfileName, setNewProfileName] = useState(''); const [isLoading, setIsLoading] = useState(true); + // CustomAlert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); + }; + // Load profiles from AsyncStorage const loadProfiles = useCallback(async () => { try { @@ -59,7 +76,7 @@ const ProfilesScreen: React.FC = () => { } } catch (error) { if (__DEV__) console.error('Error loading profiles:', error); - Alert.alert('Error', 'Failed to load profiles'); + openAlert('Error', 'Failed to load profiles'); } finally { setIsLoading(false); } @@ -85,7 +102,7 @@ const ProfilesScreen: React.FC = () => { await AsyncStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles)); } catch (error) { if (__DEV__) console.error('Error saving profiles:', error); - Alert.alert('Error', 'Failed to save profiles'); + openAlert('Error', 'Failed to save profiles'); } }, []); @@ -101,7 +118,7 @@ const ProfilesScreen: React.FC = () => { const handleAddProfile = useCallback(() => { if (!newProfileName.trim()) { - Alert.alert('Error', 'Please enter a profile name'); + openAlert('Error', 'Please enter a profile name'); return; } @@ -133,24 +150,23 @@ const ProfilesScreen: React.FC = () => { // Prevent deleting the active profile const isActiveProfile = profiles.find(p => p.id === id)?.isActive; if (isActiveProfile) { - Alert.alert('Error', 'Cannot delete the active profile. Switch to another profile first.'); + openAlert('Error', 'Cannot delete the active profile. Switch to another profile first.'); return; } // Prevent deleting the last profile if (profiles.length <= 1) { - Alert.alert('Error', 'Cannot delete the only profile'); + openAlert('Error', 'Cannot delete the only profile'); return; } - Alert.alert( + openAlert( 'Delete Profile', 'Are you sure you want to delete this profile? This action cannot be undone.', [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Delete', - style: 'destructive', + label: 'Delete', onPress: () => { const updatedProfiles = profiles.filter(profile => profile.id !== id); setProfiles(updatedProfiles); @@ -313,6 +329,14 @@ const ProfilesScreen: React.FC = () => { + + setAlertVisible(false)} + /> ); }; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 4d2e955..df644ac 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -8,7 +8,6 @@ import { ScrollView, SafeAreaView, StatusBar, - Alert, Platform, Dimensions, Image, @@ -31,6 +30,7 @@ import { useAccount } from '../contexts/AccountContext'; import { catalogService } from '../services/catalogService'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as Sentry from '@sentry/react-native'; +import CustomAlert from '../components/CustomAlert'; const { width, height } = Dimensions.get('window'); const isTablet = width >= 768; @@ -227,6 +227,22 @@ const Sidebar: React.FC = ({ selectedCategory, onCategorySelect, c const SettingsScreen: React.FC = () => { const { settings, updateSetting } = useSettings(); const [hasUpdateBadge, setHasUpdateBadge] = useState(false); + // CustomAlert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); + }; useEffect(() => { if (Platform.OS !== 'android') return; @@ -333,14 +349,13 @@ const SettingsScreen: React.FC = () => { }, [navigation, loadData]); const handleResetSettings = useCallback(() => { - Alert.alert( + openAlert( 'Reset Settings', 'Are you sure you want to reset all settings to default values?', [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Reset', - style: 'destructive', + label: 'Reset', onPress: () => { (Object.keys(DEFAULT_SETTINGS) as Array).forEach(key => { updateSetting(key, DEFAULT_SETTINGS[key]); @@ -352,20 +367,19 @@ const SettingsScreen: React.FC = () => { }, [updateSetting]); const handleClearMDBListCache = () => { - Alert.alert( - "Clear MDBList Cache", - "Are you sure you want to clear all cached MDBList data? This cannot be undone.", + openAlert( + 'Clear MDBList Cache', + 'Are you sure you want to clear all cached MDBList data? This cannot be undone.', [ - { text: "Cancel", style: "cancel" }, + { label: 'Cancel', onPress: () => {} }, { - text: "Clear", - style: "destructive", + label: 'Clear', onPress: async () => { try { await AsyncStorage.removeItem('mdblist_cache'); - Alert.alert("Success", "MDBList cache has been cleared."); + openAlert('Success', 'MDBList cache has been cleared.'); } catch (error) { - Alert.alert("Error", "Could not clear MDBList cache."); + openAlert('Error', 'Could not clear MDBList cache.'); if (__DEV__) console.error('Error clearing MDBList cache:', error); } } @@ -637,9 +651,9 @@ const SettingsScreen: React.FC = () => { onPress={async () => { try { await AsyncStorage.removeItem('hasCompletedOnboarding'); - Alert.alert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.'); + openAlert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.'); } catch (error) { - Alert.alert('Error', 'Failed to reset onboarding.'); + openAlert('Error', 'Failed to reset onboarding.'); } }} renderControl={ChevronRight} @@ -649,20 +663,19 @@ const SettingsScreen: React.FC = () => { title="Clear All Data" icon="delete-forever" onPress={() => { - Alert.alert( + openAlert( 'Clear All Data', 'This will reset all settings and clear all cached data. Are you sure?', [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Clear', - style: 'destructive', + label: 'Clear', onPress: async () => { try { await AsyncStorage.clear(); - Alert.alert('Success', 'All data cleared. Please restart the app.'); + openAlert('Success', 'All data cleared. Please restart the app.'); } catch (error) { - Alert.alert('Error', 'Failed to clear data.'); + openAlert('Error', 'Failed to clear data.'); } } } @@ -786,6 +799,13 @@ const SettingsScreen: React.FC = () => { + setAlertVisible(false)} + /> ); } @@ -863,6 +883,13 @@ const SettingsScreen: React.FC = () => { + setAlertVisible(false)} + /> ); }; diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 49bf775..59a6811 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -11,7 +11,6 @@ import { ImageBackground, ScrollView, StatusBar, - Alert, Dimensions, Linking, Clipboard, @@ -47,6 +46,7 @@ import { useSettings } from '../hooks/useSettings'; import QualityBadge from '../components/metadata/QualityBadge'; import { logger } from '../utils/logger'; import { isMkvStream } from '../utils/mkvDetection'; +import CustomAlert from '../components/CustomAlert'; const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906'; const HDR_ICON = 'https://uxwing.com/wp-content/themes/uxwing/download/video-photography-multimedia/hdr-icon.png'; @@ -201,7 +201,7 @@ const AnimatedView = memo(({ }); // Extracted Components -const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo }: { +const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo, showAlert }: { stream: Stream; onPress: () => void; index: number; @@ -210,6 +210,7 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the theme: any; showLogos?: boolean; scraperLogo?: string | null; + showAlert: (title: string, message: string) => void; }) => { // Handle long press to copy stream URL to clipboard @@ -217,18 +218,10 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the if (stream.url) { try { await Clipboard.setString(stream.url); - Alert.alert( - 'Copied!', - 'Stream URL has been copied to clipboard.', - [{ text: 'OK' }] - ); + showAlert('Copied!', 'Stream URL has been copied to clipboard.'); } catch (error) { // Fallback: show URL in alert if clipboard fails - Alert.alert( - 'Stream URL', - stream.url, - [{ text: 'OK' }] - ); + showAlert('Stream URL', stream.url); } } }, [stream.url]); @@ -439,6 +432,23 @@ export const StreamsScreen = () => { const loadStartTimeRef = useRef(0); const hasDoneInitialLoadRef = useRef(false); + // CustomAlert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); + }; + // Track when we started fetching streams so we can show an extended loading state @@ -953,7 +963,7 @@ export const StreamsScreen = () => { // Block magnet links - not supported yet if (stream.url.startsWith('magnet:')) { try { - Alert.alert('Not supported', 'Torrent streaming is not supported yet.'); + openAlert('Not supported', 'Torrent streaming is not supported yet.'); } catch (_e) {} return; } @@ -1864,6 +1874,7 @@ export const StreamsScreen = () => { theme={currentTheme} showLogos={settings.showScraperLogos} scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogoCache.get((item.addonId || (item as any).addon) as string) || null : null} + showAlert={(t, m) => openAlert(t, m)} /> )} @@ -1907,6 +1918,13 @@ export const StreamsScreen = () => { )} + setAlertVisible(false)} + /> ); }; diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index 660ecd3..d3e1f3c 100644 --- a/src/screens/TMDBSettingsScreen.tsx +++ b/src/screens/TMDBSettingsScreen.tsx @@ -8,7 +8,6 @@ import { SafeAreaView, StatusBar, Platform, - Alert, ActivityIndicator, Linking, ScrollView, @@ -27,6 +26,7 @@ import { useSettings } from '../hooks/useSettings'; import { logger } from '../utils/logger'; import { useTheme } from '../contexts/ThemeContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import CustomAlert from '../components/CustomAlert'; const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key'; const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key'; @@ -39,10 +39,37 @@ const TMDBSettingsScreen = () => { const [useCustomKey, setUseCustomKey] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const [isInputFocused, setIsInputFocused] = useState(false); + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([ + { label: 'OK', onPress: () => setAlertVisible(false) }, + ]); const apiKeyInputRef = useRef(null); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress?: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + if (actions && actions.length > 0) { + setAlertActions( + actions.map(a => ({ + label: a.label, + style: a.style, + onPress: () => { a.onPress?.(); }, + })) + ); + } else { + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + } + setAlertVisible(true); + }; + useEffect(() => { logger.log('[TMDBSettingsScreen] Component mounted'); loadSettings(); @@ -135,18 +162,16 @@ const TMDBSettingsScreen = () => { const clearApiKey = async () => { logger.log('[TMDBSettingsScreen] Clear API key requested'); - Alert.alert( + openAlert( 'Clear API Key', 'Are you sure you want to remove your custom API key and revert to the default?', [ - { - text: 'Cancel', - style: 'cancel', - onPress: () => logger.log('[TMDBSettingsScreen] Clear API key cancelled') + { + label: 'Cancel', + onPress: () => logger.log('[TMDBSettingsScreen] Clear API key cancelled'), }, { - text: 'Clear', - style: 'destructive', + label: 'Clear', onPress: async () => { logger.log('[TMDBSettingsScreen] Proceeding with API key clear'); try { @@ -159,10 +184,10 @@ const TMDBSettingsScreen = () => { logger.log('[TMDBSettingsScreen] API key cleared successfully'); } catch (error) { logger.error('[TMDBSettingsScreen] Failed to clear API key:', error); - Alert.alert('Error', 'Failed to clear API key'); + openAlert('Error', 'Failed to clear API key'); } - } - } + }, + }, ] ); }; @@ -240,7 +265,7 @@ const TMDBSettingsScreen = () => { } return ( - + @@ -404,6 +429,13 @@ const TMDBSettingsScreen = () => { )} + setAlertVisible(false)} + actions={alertActions} + /> ); }; diff --git a/src/screens/ThemeScreen.tsx b/src/screens/ThemeScreen.tsx index 6188449..7cfacf9 100644 --- a/src/screens/ThemeScreen.tsx +++ b/src/screens/ThemeScreen.tsx @@ -6,7 +6,6 @@ import { TouchableOpacity, Switch, ScrollView, - Alert, Platform, TextInput, Dimensions, @@ -23,6 +22,7 @@ import { colors } from '../styles/colors'; import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext'; import { RootStackParamList } from '../navigation/AppNavigator'; import { useSettings } from '../hooks/useSettings'; +import CustomAlert from '../components/CustomAlert'; const { width } = Dimensions.get('window'); @@ -153,10 +153,20 @@ interface ThemeColorEditorProps { onCancel: () => void; } -const ThemeColorEditor: React.FC = ({ +// Accept alert state setters as props +const ThemeColorEditor: React.FC void; + setAlertMessage: (s: string) => void; + setAlertActions: (a: any[]) => void; + setAlertVisible: (v: boolean) => void; +}> = ({ initialColors, onSave, - onCancel + onCancel, + setAlertTitle, + setAlertMessage, + setAlertActions, + setAlertVisible }) => { const [themeName, setThemeName] = useState('Custom Theme'); const [selectedColorKey, setSelectedColorKey] = useState('primary'); @@ -175,7 +185,10 @@ const ThemeColorEditor: React.FC = ({ const handleSave = () => { if (!themeName.trim()) { - Alert.alert('Invalid Name', 'Please enter a valid theme name'); + setAlertTitle('Invalid Name'); + setAlertMessage('Please enter a valid theme name'); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); return; } onSave({ @@ -318,7 +331,11 @@ const ThemeScreen: React.FC = () => { const [isEditMode, setIsEditMode] = useState(false); const [editingTheme, setEditingTheme] = useState(null); const [activeFilter, setActiveFilter] = useState('all'); - + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); + // Force consistent status bar settings useEffect(() => { const applyStatusBarConfig = () => { @@ -373,19 +390,18 @@ const ThemeScreen: React.FC = () => { }, []); const handleDeleteTheme = useCallback((theme: Theme) => { - Alert.alert( - 'Delete Theme', - `Are you sure you want to delete "${theme.name}"?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: () => deleteCustomTheme(theme.id) - } - ] - ); - }, [deleteCustomTheme]); + setAlertTitle('Delete Theme'); + setAlertMessage(`Are you sure you want to delete "${theme.name}"?`); + setAlertActions([ + { label: 'Cancel', style: { color: '#888' }, onPress: () => {} }, + { + label: 'Delete', + style: { color: currentTheme.colors.error }, + onPress: () => deleteCustomTheme(theme.id), + }, + ]); + setAlertVisible(true); + }, [deleteCustomTheme, currentTheme.colors.error]); const handleCreateTheme = useCallback(() => { setEditingTheme(null); @@ -427,6 +443,33 @@ const ThemeScreen: React.FC = () => { setEditingTheme(null); }, []); + // Pass alert state to ThemeColorEditor + const ThemeColorEditorWithAlert = (props: any) => { + const handleSave = (themeName: string, themeColors: any, onSave: any) => { + if (!themeName.trim()) { + setAlertTitle('Invalid Name'); + setAlertMessage('Please enter a valid theme name'); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); + return false; + } + onSave(); + return true; + }; + return ( + <> + + setAlertVisible(false)} + /> + + ); + }; + if (isEditMode) { const initialColors = editingTheme ? { primary: editingTheme.colors.primary, @@ -441,15 +484,24 @@ const ThemeScreen: React.FC = () => { return ( + setAlertVisible(false)} /> ); @@ -458,9 +510,7 @@ const ThemeScreen: React.FC = () => { return ( @@ -553,6 +603,14 @@ const ThemeScreen: React.FC = () => { /> + + setAlertVisible(false)} + /> ); }; @@ -949,4 +1007,4 @@ const styles = StyleSheet.create({ }, }); -export default ThemeScreen; \ No newline at end of file +export default ThemeScreen; \ No newline at end of file diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index cfef672..d6d6316 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -5,7 +5,6 @@ import { StyleSheet, TouchableOpacity, ActivityIndicator, - Alert, Image, SafeAreaView, ScrollView, @@ -25,6 +24,7 @@ import { useTheme } from '../contexts/ThemeContext'; import { useTraktIntegration } from '../hooks/useTraktIntegration'; import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings'; import { colors } from '../styles'; +import CustomAlert from '../components/CustomAlert'; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; @@ -68,6 +68,33 @@ const TraktSettingsScreen: React.FC = () => { const [showSyncFrequencyModal, setShowSyncFrequencyModal] = useState(false); const [showThresholdModal, setShowThresholdModal] = useState(false); + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([ + { label: 'OK', onPress: () => setAlertVisible(false) }, + ]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress?: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + if (actions && actions.length > 0) { + setAlertActions( + actions.map(a => ({ + label: a.label, + style: a.style, + onPress: () => { a.onPress?.(); }, + })) + ); + } else { + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + } + setAlertVisible(true); + }; const checkAuthStatus = useCallback(async () => { setIsLoading(true); @@ -120,32 +147,32 @@ const TraktSettingsScreen: React.FC = () => { logger.log('[TraktSettingsScreen] Token exchange successful'); checkAuthStatus().then(() => { // Show success message - Alert.alert( + openAlert( 'Successfully Connected', 'Your Trakt account has been connected successfully.', [ { - text: 'OK', - onPress: () => navigation.goBack() + label: 'OK', + onPress: () => navigation.goBack(), } ] ); }); } else { logger.error('[TraktSettingsScreen] Token exchange failed'); - Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.'); + openAlert('Authentication Error', 'Failed to complete authentication with Trakt.'); } }) .catch(error => { logger.error('[TraktSettingsScreen] Token exchange error:', error); - Alert.alert('Authentication Error', 'An error occurred during authentication.'); + openAlert('Authentication Error', 'An error occurred during authentication.'); }) .finally(() => { setIsExchangingCode(false); }); } else if (response.type === 'error') { logger.error('[TraktSettingsScreen] Authentication error:', response.error); - Alert.alert('Authentication Error', response.error?.message || 'An error occurred during authentication.'); + openAlert('Authentication Error', response.error?.message || 'An error occurred during authentication.'); setIsExchangingCode(false); } else { logger.log('[TraktSettingsScreen] Auth response type:', response.type); @@ -159,14 +186,13 @@ const TraktSettingsScreen: React.FC = () => { }; const handleSignOut = async () => { - Alert.alert( + openAlert( 'Sign Out', 'Are you sure you want to sign out of your Trakt account?', [ - { text: 'Cancel', style: 'cancel' }, + { label: 'Cancel', onPress: () => {} }, { - text: 'Sign Out', - style: 'destructive', + label: 'Sign Out', onPress: async () => { setIsLoading(true); try { @@ -175,7 +201,7 @@ const TraktSettingsScreen: React.FC = () => { setUserProfile(null); } catch (error) { logger.error('[TraktSettingsScreen] Error signing out:', error); - Alert.alert('Error', 'Failed to sign out of Trakt.'); + openAlert('Error', 'Failed to sign out of Trakt.'); } finally { setIsLoading(false); } @@ -398,10 +424,9 @@ const TraktSettingsScreen: React.FC = () => { disabled={isSyncing} onPress={async () => { const success = await performManualSync(); - Alert.alert( + openAlert( 'Sync Complete', - success ? 'Successfully synced your watch progress with Trakt.' : 'Sync failed. Please try again.', - [{ text: 'OK' }] + success ? 'Successfully synced your watch progress with Trakt.' : 'Sync failed. Please try again.' ); }} > diff --git a/src/screens/UpdateScreen.tsx b/src/screens/UpdateScreen.tsx index 2a4fd4a..07e2133 100644 --- a/src/screens/UpdateScreen.tsx +++ b/src/screens/UpdateScreen.tsx @@ -7,7 +7,6 @@ import { ScrollView, SafeAreaView, StatusBar, - Alert, Platform, Dimensions } from 'react-native'; @@ -19,6 +18,7 @@ import { RootStackParamList } from '../navigation/AppNavigator'; import { useTheme } from '../contexts/ThemeContext'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import UpdateService from '../services/updateService'; +import CustomAlert from '../components/CustomAlert'; import AsyncStorage from '@react-native-async-storage/async-storage'; const { width, height } = Dimensions.get('window'); @@ -65,7 +65,36 @@ const UpdateScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - + + // CustomAlert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState void; style?: object }>>([ + { label: 'OK', onPress: () => setAlertVisible(false) }, + ]); + + const openAlert = ( + title: string, + message: string, + actions?: Array<{ label: string; onPress?: () => void; style?: object }> + ) => { + setAlertTitle(title); + setAlertMessage(message); + if (actions && actions.length > 0) { + setAlertActions( + actions.map(a => ({ + label: a.label, + style: a.style, + onPress: () => { a.onPress?.(); }, + })) + ); + } else { + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + } + setAlertVisible(true); + }; + const [updateInfo, setUpdateInfo] = useState(null); const [currentInfo, setCurrentInfo] = useState(null); const [isChecking, setIsChecking] = useState(false); @@ -100,7 +129,7 @@ const UpdateScreen: React.FC = () => { if (__DEV__) console.error('Error checking for updates:', error); setUpdateStatus('error'); setLastOperation(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); - Alert.alert('Error', 'Failed to check for updates'); + openAlert('Error', 'Failed to check for updates'); } finally { setIsChecking(false); } @@ -149,17 +178,17 @@ const UpdateScreen: React.FC = () => { if (success) { setUpdateStatus('success'); setLastOperation('Update installed successfully'); - Alert.alert('Success', 'Update will be applied on next app restart'); + openAlert('Success', 'Update will be applied on next app restart'); } else { setUpdateStatus('error'); setLastOperation('No update available to install'); - Alert.alert('No Update', 'No update available to install'); + openAlert('No Update', 'No update available to install'); } } catch (error) { if (__DEV__) console.error('Error installing update:', error); setUpdateStatus('error'); setLastOperation(`Installation error: ${error instanceof Error ? error.message : 'Unknown error'}`); - Alert.alert('Error', 'Failed to install update'); + openAlert('Error', 'Failed to install update'); } finally { setIsInstalling(false); } @@ -569,6 +598,13 @@ const UpdateScreen: React.FC = () => { )} + setAlertVisible(false)} + actions={alertActions} + /> ); };