Merge remote-tracking branch 'origin/main'

This commit is contained in:
tapframe 2025-09-22 18:47:33 +05:30
commit 00937f37ec
23 changed files with 1029 additions and 415 deletions

11
package-lock.json generated
View file

@ -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",

View file

@ -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 (
<Modal
visible={visible}
transparent
animationType="none"
onRequestClose={onClose}
>
<Animated.View style={[styles.overlay, { backgroundColor: themeColors.transparentDark }, overlayStyle]}>
<Pressable style={styles.overlayPressable} onPress={onClose} />
<View style={styles.centered}>
<Animated.View style={[styles.alertContainer, alertStyle, { backgroundColor, borderColor }]}>
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
<Text style={[styles.message, { color: textColor }]}>{message}</Text>
<View style={styles.actionsRow}>
{actions.map((action, idx) => (
<TouchableOpacity
key={action.label}
style={[styles.actionButton, idx === actions.length - 1 && styles.lastActionButton, action.style]}
onPress={() => {
action.onPress();
onClose();
}}
>
<Text style={[styles.actionText, { color: themeColors.primary }]}>{action.label}</Text>
</TouchableOpacity>
))}
</View>
</Animated.View>
</View>
</Animated.View>
</Modal>
);
};
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;

View file

@ -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<ContinueWatchingRef>((props, re
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Alert state for CustomAlert
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
// 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<ContinueWatchingRef>((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<ContinueWatchingRef>((props, re
onEndReached={() => {}}
removeClippedSubviews={true}
/>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</Animated.View>
);
});

View file

@ -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<MoreLikeThisSectionProps> = ({
const { currentTheme } = useTheme();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const [alertVisible, setAlertVisible] = React.useState(false);
const [alertTitle, setAlertTitle] = React.useState('');
const [alertMessage, setAlertMessage] = React.useState('');
const [alertActions, setAlertActions] = React.useState<any[]>([]);
const handleItemPress = async (item: StreamingContent) => {
try {
// Extract TMDB ID from the tmdb:123456 format
@ -80,11 +85,10 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
}
} 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<MoreLikeThisSectionProps> = ({
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.listContentContainer}
/>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</View>
);
};
@ -169,4 +180,4 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
},
});
});

View file

@ -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 {

View file

@ -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<SuggestionChipProps> = 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<Array<{ label: string; onPress: () => 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<AIChatScreenRouteProp>();
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 */}
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
}
};
@ -786,6 +823,13 @@ const AIChatScreen: React.FC = () => {
</SafeAreaView>
</KeyboardAvoidingView>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</Animated.View>
);
};

View file

@ -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<Array<{ label: string; onPress: () => 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 (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
{/* Header */}
@ -344,6 +371,13 @@ const AISettingsScreen: React.FC = () => {
<SvgXml xml={OPENROUTER_SVG.replace(/CURRENTCOLOR/g, currentTheme.colors.mediumEmphasis)} width={180} height={60} />
</View>
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -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<any[]>([]);
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 = () => {
<Text style={styles.signOutText}>Sign out</Text>
</TouchableOpacity>
</Animated.View>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</View>
);
};

View file

@ -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<any[]>([]);
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 = () => {
</View>
</View>
</Modal>
</SafeAreaView>
{/* Custom Alert Modal */}
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -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<any[]>([]);
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 */}
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</View>
);
};
export default CastMoviesScreen;

View file

@ -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<CatalogSetting | null>(null);
const [currentRenameValue, setCurrentRenameValue] = useState('');
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
// 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 = () => {
)}
</Modal>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
);
};
export default CatalogSettingsScreen;
export default CatalogSettingsScreen;

View file

@ -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<any[]>([]);
const [loading, setLoading] = useState(true);
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(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 = () => {
</ScrollView>
</>
)}
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -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<Array<{ label: string; onPress: () => 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 (
<SafeAreaView style={[styles.container]}>
<StatusBar barStyle="light-content" />
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
@ -703,14 +728,11 @@ const LogoSourceSettings = () => {
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
<Text style={styles.backText}>Settings</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={styles.headerTitle}>Logo Source</Text>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
@ -866,6 +888,13 @@ const LogoSourceSettings = () => {
</Text>
</View>
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -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<any[]>([]);
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 = () => {
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -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<any[]>([]);
// 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 = () => {
)}
</Animated.View>
</ScrollView>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -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<Array<{ label: string; onPress: () => 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<ScraperInfo[]>([]);
@ -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');
}}
>
<Text style={styles.buttonText}>Save</Text>
@ -1924,6 +1938,13 @@ const PluginsScreen: React.FC = () => {
</View>
</View>
</Modal>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
);
};

View file

@ -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<Array<{ label: string; onPress: () => 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 = () => {
</View>
</View>
</Modal>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
);
};

View file

@ -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<SidebarProps> = ({ 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<Array<{ label: string; onPress: () => 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<keyof typeof DEFAULT_SETTINGS>).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 = () => {
</ScrollView>
</View>
</View>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</View>
);
}
@ -863,6 +883,13 @@ const SettingsScreen: React.FC = () => {
</ScrollView>
</View>
</View>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</View>
);
};

View file

@ -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<Array<{ label: string; onPress: () => 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)}
/>
</View>
)}
@ -1907,6 +1918,13 @@ export const StreamsScreen = () => {
</View>
)}
</View>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</View>
);
};

View file

@ -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<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
]);
const apiKeyInputRef = useRef<TextInput>(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 (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
<View style={styles.header}>
@ -404,6 +429,13 @@ const TMDBSettingsScreen = () => {
</View>
)}
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</View>
);
};

View file

@ -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<ThemeColorEditorProps> = ({
// Accept alert state setters as props
const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
setAlertTitle: (s: string) => 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<ColorKey>('primary');
@ -175,7 +185,10 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps> = ({
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<Theme | null>(null);
const [activeFilter, setActiveFilter] = useState('all');
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
// 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 (
<>
<ThemeColorEditor {...props} handleSave={handleSave} />
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</>
);
};
if (isEditMode) {
const initialColors = editingTheme ? {
primary: editingTheme.colors.primary,
@ -441,15 +484,24 @@ const ThemeScreen: React.FC = () => {
return (
<SafeAreaView style={[
styles.container,
{
backgroundColor: currentTheme.colors.darkBackground,
}
{ backgroundColor: currentTheme.colors.darkBackground }
]}>
<StatusBar barStyle="light-content" />
<ThemeColorEditor
initialColors={initialColors}
onSave={handleSaveTheme}
onCancel={handleCancelEdit}
setAlertTitle={setAlertTitle}
setAlertMessage={setAlertMessage}
setAlertActions={setAlertActions}
setAlertVisible={setAlertVisible}
/>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
);
@ -458,9 +510,7 @@ const ThemeScreen: React.FC = () => {
return (
<SafeAreaView style={[
styles.container,
{
backgroundColor: currentTheme.colors.darkBackground,
}
{ backgroundColor: currentTheme.colors.darkBackground }
]}>
<StatusBar barStyle="light-content" />
@ -553,6 +603,14 @@ const ThemeScreen: React.FC = () => {
/>
</View>
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
);
};
@ -949,4 +1007,4 @@ const styles = StyleSheet.create({
},
});
export default ThemeScreen;
export default ThemeScreen;

View file

@ -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<Array<{ label: string; onPress: () => 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.'
);
}}
>

View file

@ -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<NavigationProp<RootStackParamList>>();
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<Array<{ label: string; onPress: () => 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<any>(null);
const [currentInfo, setCurrentInfo] = useState<any>(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 = () => {
)}
</ScrollView>
</View>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};