diff --git a/package-lock.json b/package-lock.json index 0e57348a..87c2e9a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14942,17 +14942,6 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "license": "MIT" }, - "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", diff --git a/src/components/CustomAlert.tsx b/src/components/CustomAlert.tsx new file mode 100644 index 00000000..7540ed51 --- /dev/null +++ b/src/components/CustomAlert.tsx @@ -0,0 +1,166 @@ +import React, { useEffect } from 'react'; +import { + Modal, + View, + Text, + StyleSheet, + Pressable, + TouchableOpacity, + useColorScheme, + Platform, +} from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated'; +import { useTheme } from '../contexts/ThemeContext'; + +interface CustomAlertProps { + visible: boolean; + title: string; + message: string; + onClose: () => void; + actions?: Array<{ + label: string; + onPress: () => void; + style?: object; + }>; +} + +export const CustomAlert = ({ + visible, + title, + message, + onClose, + actions = [ + { label: 'OK', onPress: onClose } + ], +}: CustomAlertProps) => { + const opacity = useSharedValue(0); + const scale = useSharedValue(0.95); + const isDarkMode = useColorScheme() === 'dark'; + const { currentTheme } = useTheme(); + const themeColors = currentTheme.colors; + + useEffect(() => { + const animDuration = 120; + if (visible) { + opacity.value = withTiming(1, { duration: animDuration }); + scale.value = withTiming(1, { duration: animDuration }); + } else { + opacity.value = withTiming(0, { duration: animDuration }); + scale.value = withTiming(0.95, { duration: animDuration }); + } + }, [visible]); + + const overlayStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + const alertStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + opacity: opacity.value, + })); + + const backgroundColor = isDarkMode ? themeColors.darkBackground : themeColors.elevation2 || '#FFFFFF'; + const textColor = isDarkMode ? themeColors.white : themeColors.black || '#000000'; + const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; + + return ( + + + + + + {title} + {message} + + {actions.map((action, idx) => ( + { + action.onPress(); + onClose(); + }} + > + {action.label} + + ))} + + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + overlayPressable: { + ...StyleSheet.absoluteFillObject, + }, + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + alertContainer: { + minWidth: 280, + maxWidth: '85%', + borderRadius: 20, + padding: 24, + borderWidth: 1, + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 8, + }, + android: { + elevation: 8, + }, + }), + }, + title: { + fontSize: 18, + fontWeight: '700', + marginBottom: 12, + textAlign: 'center', + }, + message: { + fontSize: 16, + marginBottom: 20, + textAlign: 'center', + }, + actionsRow: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 12, + }, + actionButton: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + }, + lastActionButton: { + // Optionally style the last button differently + }, + actionText: { + fontSize: 16, + fontWeight: '600', + }, +}); + +export default CustomAlert; diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 48bdfbca..90f42649 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -7,7 +7,6 @@ import { Dimensions, AppState, AppStateStatus, - Alert, ActivityIndicator } from 'react-native'; import { FlashList } from '@shopify/flash-list'; @@ -24,6 +23,7 @@ import { logger } from '../../utils/logger'; import * as Haptics from 'expo-haptics'; import { TraktService } from '../../services/traktService'; import { stremioService } from '../../services/stremioService'; +import CustomAlert from '../../components/CustomAlert'; // Define interface for continue watching items interface ContinueWatchingItem extends StreamingContent { @@ -96,6 +96,12 @@ const ContinueWatchingSection = React.forwardRef((props, re const [deletingItemId, setDeletingItemId] = useState(null); const longPressTimeoutRef = useRef(null); + // Alert state for CustomAlert + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); + // Use a ref to track if a background refresh is in progress to avoid state updates const isRefreshingRef = useRef(false); @@ -516,80 +522,51 @@ const ContinueWatchingSection = React.forwardRef((props, re // Ignore haptic errors } - // Show confirmation alert - Alert.alert( - "Remove from Continue Watching", - `Remove "${item.name}" from your continue watching list?`, - [ - { - text: "Cancel", - style: "cancel" - }, - { - text: "Remove", - style: "destructive", - onPress: async () => { - setDeletingItemId(item.id); - try { - // Trigger haptic feedback for confirmation - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - - // Remove all watch progress for this content (all episodes if series) - await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true }); - - // Also remove from Trakt playback queue if authenticated - const traktService = TraktService.getInstance(); - const isAuthed = await traktService.isAuthenticated(); - logger.log(`🔍 [ContinueWatching] Trakt authentication status: ${isAuthed}`); - - if (isAuthed) { - logger.log(`đŸ—‘ī¸ [ContinueWatching] Removing Trakt history for ${item.id}`); - let traktResult = false; - - if (item.type === 'movie') { - logger.log(`đŸŽŦ [ContinueWatching] Removing movie from Trakt history: ${item.name}`); - traktResult = await traktService.removeMovieFromHistory(item.id); - } else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) { - logger.log(`đŸ“ē [ContinueWatching] Removing specific episode from Trakt history: ${item.name} S${item.season}E${item.episode}`); - traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode); - } else { - logger.log(`đŸ“ē [ContinueWatching] Removing entire show from Trakt history: ${item.name} (no specific episode info)`); - traktResult = await traktService.removeShowFromHistory(item.id); - } - - logger.log(`✅ [ContinueWatching] Trakt removal result: ${traktResult}`); + setAlertTitle('Remove from Continue Watching'); + setAlertMessage(`Remove "${item.name}" from your continue watching list?`); + setAlertActions([ + { + label: 'Cancel', + style: { color: '#888' }, + onPress: () => {}, + }, + { + label: 'Remove', + style: { color: currentTheme.colors.error }, + onPress: async () => { + setDeletingItemId(item.id); + try { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true }); + const traktService = TraktService.getInstance(); + const isAuthed = await traktService.isAuthenticated(); + if (isAuthed) { + let traktResult = false; + if (item.type === 'movie') { + traktResult = await traktService.removeMovieFromHistory(item.id); + } else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) { + traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode); } else { - logger.log(`â„šī¸ [ContinueWatching] Skipping Trakt removal - not authenticated`); + traktResult = await traktService.removeShowFromHistory(item.id); } - - // Track this item as recently removed to prevent immediate re-addition - const itemKey = `${item.type}:${item.id}`; - recentlyRemovedRef.current.add(itemKey); - - // Persist the removed state for long-term tracking - await storageService.addContinueWatchingRemoved(item.id, item.type); - - // Clear from recently removed after the ignore duration - setTimeout(() => { - recentlyRemovedRef.current.delete(itemKey); - }, REMOVAL_IGNORE_DURATION); - - // Update the list by filtering out the deleted item - setContinueWatchingItems(prev => { - const newList = prev.filter(i => i.id !== item.id); - return newList; - }); - - } catch (error) { - // Continue even if removal fails - } finally { - setDeletingItemId(null); } + const itemKey = `${item.type}:${item.id}`; + recentlyRemovedRef.current.add(itemKey); + await storageService.addContinueWatchingRemoved(item.id, item.type); + setTimeout(() => { + recentlyRemovedRef.current.delete(itemKey); + }, REMOVAL_IGNORE_DURATION); + setContinueWatchingItems(prev => prev.filter(i => i.id !== item.id)); + } catch (error) { + // Continue even if removal fails + } finally { + setDeletingItemId(null); } - } - ] - ); - }, []); + }, + }, + ]); + setAlertVisible(true); + }, [currentTheme.colors.error]); // Memoized render function for continue watching items const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( @@ -730,6 +707,14 @@ const ContinueWatchingSection = React.forwardRef((props, re onEndReached={() => {}} removeClippedSubviews={true} /> + + setAlertVisible(false)} + /> ); }); diff --git a/src/screens/AddonsScreen.tsx b/src/screens/AddonsScreen.tsx index 2d6d1928..1bc8b647 100644 --- a/src/screens/AddonsScreen.tsx +++ b/src/screens/AddonsScreen.tsx @@ -7,7 +7,6 @@ import { TextInput, TouchableOpacity, ActivityIndicator, - Alert, SafeAreaView, StatusBar, Modal, @@ -30,6 +29,7 @@ import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { BlurView as ExpoBlurView } from 'expo-blur'; +import CustomAlert from '../components/CustomAlert'; // Removed community blur and expo-constants for Android overlay import axios from 'axios'; import { useTheme } from '../contexts/ThemeContext'; @@ -597,6 +597,11 @@ const AddonsScreen = () => { const [installing, setInstalling] = useState(false); const [catalogCount, setCatalogCount] = useState(0); // Add state for reorder mode + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); const [reorderMode, setReorderMode] = useState(false); // Use ThemeContext const { currentTheme } = useTheme(); @@ -662,8 +667,11 @@ const AddonsScreen = () => { setCatalogCount(totalCatalogs); } } catch (error) { - logger.error('Failed to load addons:', error); - Alert.alert('Error', 'Failed to load addons'); + logger.error('Failed to load addons:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to load addons'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setLoading(false); } @@ -683,10 +691,9 @@ const AddonsScreen = () => { setCommunityAddons(validAddons); } catch (error) { - logger.error('Failed to load community addons:', error); - setCommunityError('Failed to load community addons. Please try again later.'); - // Set empty array on error since Cinemeta is pre-installed - setCommunityAddons([]); + logger.error('Failed to load community addons:', error); + setCommunityError('Failed to load community addons. Please try again later.'); + setCommunityAddons([]); } finally { setCommunityLoading(false); } @@ -695,7 +702,10 @@ const AddonsScreen = () => { const handleAddAddon = async (url?: string) => { const urlToInstall = url || addonUrl; if (!urlToInstall) { - Alert.alert('Error', 'Please enter an addon URL or select a community addon'); + setAlertTitle('Error'); + setAlertMessage('Please enter an addon URL or select a community addon'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); return; } @@ -706,8 +716,11 @@ const AddonsScreen = () => { setAddonUrl(urlToInstall); setShowConfirmModal(true); } catch (error) { - logger.error('Failed to fetch addon details:', error); - Alert.alert('Error', `Failed to fetch addon details from ${urlToInstall}`); + logger.error('Failed to fetch addon details:', error); + setAlertTitle('Error'); + setAlertMessage(`Failed to fetch addon details from ${urlToInstall}`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setInstalling(false); } @@ -723,10 +736,16 @@ const AddonsScreen = () => { setShowConfirmModal(false); setAddonDetails(null); loadAddons(); - Alert.alert('Success', 'Addon installed successfully'); + setAlertTitle('Success'); + setAlertMessage('Addon installed successfully'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } catch (error) { - logger.error('Failed to install addon:', error); - Alert.alert('Error', 'Failed to install addon'); + logger.error('Failed to install addon:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to install addon'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setInstalling(false); } @@ -754,31 +773,26 @@ const AddonsScreen = () => { const handleRemoveAddon = (addon: ExtendedManifest) => { // Check if this is a pre-installed addon if (stremioService.isPreInstalledAddon(addon.id)) { - Alert.alert( - 'Cannot Remove Addon', - `${addon.name} is a pre-installed addon and cannot be removed.`, - [{ text: 'OK', style: 'default' }] - ); + setAlertTitle('Cannot Remove Addon'); + setAlertMessage(`${addon.name} is a pre-installed addon and cannot be removed.`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); return; } - - Alert.alert( - 'Uninstall Addon', - `Are you sure you want to uninstall ${addon.name}?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Uninstall', - style: 'destructive', - onPress: () => { - stremioService.removeAddon(addon.id); - - // Remove from addons list - setAddons(prev => prev.filter(a => a.id !== addon.id)); - }, + setAlertTitle('Uninstall Addon'); + setAlertMessage(`Are you sure you want to uninstall ${addon.name}?`); + setAlertActions([ + { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } }, + { + label: 'Uninstall', + onPress: () => { + stremioService.removeAddon(addon.id); + setAddons(prev => prev.filter(a => a.id !== addon.id)); }, - ] - ); + style: { color: colors.error } + }, + ]); + setAlertVisible(true); }; // Add function to handle configuration @@ -876,11 +890,10 @@ const AddonsScreen = () => { // If we couldn't determine a config URL, show an error if (!configUrl) { logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`); - Alert.alert( - 'Configuration Unavailable', - 'Could not determine configuration URL for this addon.', - [{ text: 'OK' }] - ); + setAlertTitle('Configuration Unavailable'); + setAlertMessage('Could not determine configuration URL for this addon.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); return; } @@ -893,15 +906,17 @@ const AddonsScreen = () => { Linking.openURL(configUrl); } else { logger.error(`URL cannot be opened: ${configUrl}`); - Alert.alert( - 'Cannot Open Configuration', - `The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`, - [{ text: 'OK' }] - ); + setAlertTitle('Cannot Open Configuration'); + setAlertMessage(`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } }).catch(err => { logger.error(`Error checking if URL can be opened: ${configUrl}`, err); - Alert.alert('Error', 'Could not open configuration page.'); + setAlertTitle('Error'); + setAlertMessage('Could not open configuration page.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); }); }; @@ -1482,7 +1497,15 @@ const AddonsScreen = () => { - + {/* Custom Alert Modal */} + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/HeroCatalogsScreen.tsx b/src/screens/HeroCatalogsScreen.tsx index 79514c8d..bf39e113 100644 --- a/src/screens/HeroCatalogsScreen.tsx +++ b/src/screens/HeroCatalogsScreen.tsx @@ -11,9 +11,9 @@ import { Platform, useColorScheme, ActivityIndicator, - Alert, Animated } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import { useSettings, settingsEmitter } from '../hooks/useSettings'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; @@ -35,6 +35,11 @@ const HeroCatalogsScreen: React.FC = () => { const systemColorScheme = useColorScheme(); const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode; const navigation = useNavigation(); + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); const [loading, setLoading] = useState(true); const [catalogs, setCatalogs] = useState([]); const [selectedCatalogs, setSelectedCatalogs] = useState(settings.selectedHeroCatalogs || []); @@ -120,7 +125,10 @@ const HeroCatalogsScreen: React.FC = () => { setCatalogs(catalogItems); } catch (error) { if (__DEV__) console.error('Failed to load catalogs:', error); - Alert.alert('Error', 'Failed to load catalogs'); + setAlertTitle('Error'); + setAlertMessage('Failed to load catalogs'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setLoading(false); } @@ -276,7 +284,14 @@ const HeroCatalogsScreen: React.FC = () => { )} - + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/MDBListSettingsScreen.tsx b/src/screens/MDBListSettingsScreen.tsx index 6634d1a1..019044b5 100644 --- a/src/screens/MDBListSettingsScreen.tsx +++ b/src/screens/MDBListSettingsScreen.tsx @@ -8,7 +8,6 @@ import { SafeAreaView, StatusBar, Platform, - Alert, ActivityIndicator, Linking, ScrollView, @@ -16,6 +15,7 @@ import { Clipboard, Switch, } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import { useNavigation } from '@react-navigation/native'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -361,6 +361,11 @@ const MDBListSettingsScreen = () => { const colors = currentTheme.colors; const styles = createStyles(colors); + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); const [apiKey, setApiKey] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isKeySet, setIsKeySet] = useState(false); @@ -492,34 +497,32 @@ const MDBListSettingsScreen = () => { const clearApiKey = async () => { logger.log('[MDBListSettingsScreen] Clear API key requested'); - Alert.alert( - 'Clear API Key', - 'Are you sure you want to remove the saved API key?', - [ - { - text: 'Cancel', - style: 'cancel', - onPress: () => logger.log('[MDBListSettingsScreen] Clear API key cancelled') - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - logger.log('[MDBListSettingsScreen] Proceeding with API key clear'); - try { - await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY); - setApiKey(''); - setIsKeySet(false); - setTestResult(null); - logger.log('[MDBListSettingsScreen] API key cleared successfully'); - } catch (error) { - logger.error('[MDBListSettingsScreen] Failed to clear API key:', error); - Alert.alert('Error', 'Failed to clear API key'); - } + setAlertTitle('Clear API Key'); + setAlertMessage('Are you sure you want to remove the saved API key?'); + setAlertActions([ + { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } }, + { + label: 'Clear', + onPress: async () => { + logger.log('[MDBListSettingsScreen] Proceeding with API key clear'); + try { + await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY); + setApiKey(''); + setIsKeySet(false); + setTestResult(null); + logger.log('[MDBListSettingsScreen] API key cleared successfully'); + } catch (error) { + logger.error('[MDBListSettingsScreen] Failed to clear API key:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to clear API key'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } - } - ] - ); + }, + style: { color: colors.error } + }, + ]); + setAlertVisible(true); }; const pasteFromClipboard = async () => { @@ -823,7 +826,14 @@ const MDBListSettingsScreen = () => { - + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/NotificationSettingsScreen.tsx b/src/screens/NotificationSettingsScreen.tsx index 2708c554..15568cc3 100644 --- a/src/screens/NotificationSettingsScreen.tsx +++ b/src/screens/NotificationSettingsScreen.tsx @@ -6,11 +6,11 @@ import { ScrollView, Switch, TouchableOpacity, - Alert, SafeAreaView, StatusBar, Platform, } from 'react-native'; +import CustomAlert from '../components/CustomAlert'; import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; import { notificationService, NotificationSettings } from '../services/notificationService'; @@ -36,6 +36,11 @@ const NotificationSettingsScreen = () => { const [isSyncing, setIsSyncing] = useState(false); const [notificationStats, setNotificationStats] = useState({ total: 0, upcoming: 0, thisWeek: 0 }); + // Custom alert state + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); // Load settings and stats on mount useEffect(() => { const loadSettings = async () => { @@ -104,7 +109,10 @@ const NotificationSettingsScreen = () => { setSettings(updatedSettings); } catch (error) { logger.error('Error updating notification settings:', error); - Alert.alert('Error', 'Failed to update notification settings'); + setAlertTitle('Error'); + setAlertMessage('Failed to update notification settings'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } }; @@ -114,33 +122,34 @@ const NotificationSettingsScreen = () => { }; const resetAllNotifications = async () => { - Alert.alert( - 'Reset Notifications', - 'This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Reset', - style: 'destructive', - onPress: async () => { - try { - // Cancel all notifications for all series, but do not remove from saved - const scheduledNotifications = notificationService.getScheduledNotifications?.() || []; - for (const notification of scheduledNotifications) { - await notificationService.cancelNotification(notification.id); - } - Alert.alert('Success', 'All notifications have been reset'); - } catch (error) { - logger.error('Error resetting notifications:', error); - Alert.alert('Error', 'Failed to reset notifications'); + setAlertTitle('Reset Notifications'); + setAlertMessage('This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?'); + setAlertActions([ + { label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: currentTheme.colors.mediumGray } }, + { + label: 'Reset', + onPress: async () => { + try { + const scheduledNotifications = notificationService.getScheduledNotifications?.() || []; + for (const notification of scheduledNotifications) { + await notificationService.cancelNotification(notification.id); } - }, + setAlertTitle('Success'); + setAlertMessage('All notifications have been reset'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + } catch (error) { + logger.error('Error resetting notifications:', error); + setAlertTitle('Error'); + setAlertMessage('Failed to reset notifications'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); + } }, - ] - ); + style: { color: currentTheme.colors.error } + }, + ]); + setAlertVisible(true); }; const handleSyncNotifications = async () => { @@ -154,13 +163,16 @@ const NotificationSettingsScreen = () => { const stats = notificationService.getNotificationStats(); setNotificationStats(stats); - Alert.alert( - 'Sync Complete', - `Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes` - ); + setAlertTitle('Sync Complete'); + setAlertMessage(`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } catch (error) { logger.error('Error syncing notifications:', error); - Alert.alert('Error', 'Failed to sync notifications. Please try again.'); + setAlertTitle('Error'); + setAlertMessage('Failed to sync notifications. Please try again.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } finally { setIsSyncing(false); } @@ -212,13 +224,22 @@ const NotificationSettingsScreen = () => { if (notificationId) { setTestNotificationId(notificationId); setCountdown(0); // No countdown for instant notification - Alert.alert('Success', 'Test notification scheduled to fire instantly'); + setAlertTitle('Success'); + setAlertMessage('Test notification scheduled to fire instantly'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } else { - Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.'); + setAlertTitle('Error'); + setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } } catch (error) { logger.error('Error scheduling test notification:', error); - Alert.alert('Error', 'Failed to schedule test notification'); + setAlertTitle('Error'); + setAlertMessage('Failed to schedule test notification'); + setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]); + setAlertVisible(true); } }; @@ -475,7 +496,14 @@ const NotificationSettingsScreen = () => { )} - + setAlertVisible(false)} + actions={alertActions} + /> + ); }; diff --git a/src/screens/ThemeScreen.tsx b/src/screens/ThemeScreen.tsx index 6188449a..7cfacf9c 100644 --- a/src/screens/ThemeScreen.tsx +++ b/src/screens/ThemeScreen.tsx @@ -6,7 +6,6 @@ import { TouchableOpacity, Switch, ScrollView, - Alert, Platform, TextInput, Dimensions, @@ -23,6 +22,7 @@ import { colors } from '../styles/colors'; import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext'; import { RootStackParamList } from '../navigation/AppNavigator'; import { useSettings } from '../hooks/useSettings'; +import CustomAlert from '../components/CustomAlert'; const { width } = Dimensions.get('window'); @@ -153,10 +153,20 @@ interface ThemeColorEditorProps { onCancel: () => void; } -const ThemeColorEditor: React.FC = ({ +// Accept alert state setters as props +const ThemeColorEditor: React.FC void; + setAlertMessage: (s: string) => void; + setAlertActions: (a: any[]) => void; + setAlertVisible: (v: boolean) => void; +}> = ({ initialColors, onSave, - onCancel + onCancel, + setAlertTitle, + setAlertMessage, + setAlertActions, + setAlertVisible }) => { const [themeName, setThemeName] = useState('Custom Theme'); const [selectedColorKey, setSelectedColorKey] = useState('primary'); @@ -175,7 +185,10 @@ const ThemeColorEditor: React.FC = ({ const handleSave = () => { if (!themeName.trim()) { - Alert.alert('Invalid Name', 'Please enter a valid theme name'); + setAlertTitle('Invalid Name'); + setAlertMessage('Please enter a valid theme name'); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); return; } onSave({ @@ -318,7 +331,11 @@ const ThemeScreen: React.FC = () => { const [isEditMode, setIsEditMode] = useState(false); const [editingTheme, setEditingTheme] = useState(null); const [activeFilter, setActiveFilter] = useState('all'); - + const [alertVisible, setAlertVisible] = useState(false); + const [alertTitle, setAlertTitle] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [alertActions, setAlertActions] = useState([]); + // Force consistent status bar settings useEffect(() => { const applyStatusBarConfig = () => { @@ -373,19 +390,18 @@ const ThemeScreen: React.FC = () => { }, []); const handleDeleteTheme = useCallback((theme: Theme) => { - Alert.alert( - 'Delete Theme', - `Are you sure you want to delete "${theme.name}"?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: () => deleteCustomTheme(theme.id) - } - ] - ); - }, [deleteCustomTheme]); + setAlertTitle('Delete Theme'); + setAlertMessage(`Are you sure you want to delete "${theme.name}"?`); + setAlertActions([ + { label: 'Cancel', style: { color: '#888' }, onPress: () => {} }, + { + label: 'Delete', + style: { color: currentTheme.colors.error }, + onPress: () => deleteCustomTheme(theme.id), + }, + ]); + setAlertVisible(true); + }, [deleteCustomTheme, currentTheme.colors.error]); const handleCreateTheme = useCallback(() => { setEditingTheme(null); @@ -427,6 +443,33 @@ const ThemeScreen: React.FC = () => { setEditingTheme(null); }, []); + // Pass alert state to ThemeColorEditor + const ThemeColorEditorWithAlert = (props: any) => { + const handleSave = (themeName: string, themeColors: any, onSave: any) => { + if (!themeName.trim()) { + setAlertTitle('Invalid Name'); + setAlertMessage('Please enter a valid theme name'); + setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertVisible(true); + return false; + } + onSave(); + return true; + }; + return ( + <> + + setAlertVisible(false)} + /> + + ); + }; + if (isEditMode) { const initialColors = editingTheme ? { primary: editingTheme.colors.primary, @@ -441,15 +484,24 @@ const ThemeScreen: React.FC = () => { return ( + setAlertVisible(false)} /> ); @@ -458,9 +510,7 @@ const ThemeScreen: React.FC = () => { return ( @@ -553,6 +603,14 @@ const ThemeScreen: React.FC = () => { /> + + setAlertVisible(false)} + /> ); }; @@ -949,4 +1007,4 @@ const styles = StyleSheet.create({ }, }); -export default ThemeScreen; \ No newline at end of file +export default ThemeScreen; \ No newline at end of file