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