mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
00937f37ec
23 changed files with 1029 additions and 415 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
166
src/components/CustomAlert.tsx
Normal file
166
src/components/CustomAlert.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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.'
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue