From 46b8173b41c35119fa1d33527dad7f9f09de3f3d Mon Sep 17 00:00:00 2001 From: CrissZollo Date: Sun, 21 Sep 2025 18:17:43 +0200 Subject: [PATCH] Updated all Alert.alert to the new custom one --- .../metadata/MoreLikeThisSection.tsx | 27 +++-- src/hooks/useUpdatePopup.ts | 18 ++-- src/screens/AIChatScreen.tsx | 48 ++++++++- src/screens/AISettingsScreen.tsx | 58 ++++++++--- src/screens/AccountManageScreen.tsx | 53 ++++++---- src/screens/CastMoviesScreen.tsx | 28 ++++-- src/screens/CatalogSettingsScreen.tsx | 20 +++- src/screens/LogoSourceSettings.tsx | 59 ++++++++--- src/screens/PluginsScreen.tsx | 99 +++++++++++-------- src/screens/ProfilesScreen.tsx | 44 +++++++-- src/screens/SettingsScreen.tsx | 69 +++++++++---- src/screens/StreamsScreen.tsx | 44 ++++++--- src/screens/TMDBSettingsScreen.tsx | 56 ++++++++--- src/screens/TraktSettingsScreen.tsx | 55 ++++++++--- src/screens/UpdateScreen.tsx | 48 +++++++-- 15 files changed, 533 insertions(+), 193 deletions(-) diff --git a/src/components/metadata/MoreLikeThisSection.tsx b/src/components/metadata/MoreLikeThisSection.tsx index 727cb5cf..7830f1c0 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 eb6e1917..9885190b 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 2c28ccdb..584c629e 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 e6c555c0..5a4a789e 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 1cf2ad04..f523253f 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/CastMoviesScreen.tsx b/src/screens/CastMoviesScreen.tsx index e74862ba..80e3032f 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 3461e140..28d8052a 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/LogoSourceSettings.tsx b/src/screens/LogoSourceSettings.tsx index 12daf9b8..2f5b5d08 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/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx index 7e3b1345..a1f92eb7 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 008f3b48..8d12d12d 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 4d2e9555..df644acd 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 0e1da92d..2bcaf99f 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'; @@ -176,7 +176,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; @@ -185,6 +185,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 @@ -192,18 +193,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]); @@ -413,6 +406,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 @@ -939,7 +949,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; } @@ -1847,6 +1857,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)} /> )} @@ -1885,6 +1896,13 @@ export const StreamsScreen = () => { )} + setAlertVisible(false)} + /> ); }; diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx index 660ecd39..d3e1f3c4 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/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index cfef672a..d6d63161 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 2a4fd4a3..07e21333 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} + /> ); };