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}
+ />
);
};