Updated confirmation popup to be more custom and uses the current selected theme

This commit is contained in:
CrissZollo 2025-09-21 17:19:56 +02:00
parent f961e5ac3f
commit 8d60bff989
8 changed files with 496 additions and 222 deletions

11
package-lock.json generated
View file

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

View file

@ -0,0 +1,166 @@
import React, { useEffect } from 'react';
import {
Modal,
View,
Text,
StyleSheet,
Pressable,
TouchableOpacity,
useColorScheme,
Platform,
} from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
import { useTheme } from '../contexts/ThemeContext';
interface CustomAlertProps {
visible: boolean;
title: string;
message: string;
onClose: () => void;
actions?: Array<{
label: string;
onPress: () => void;
style?: object;
}>;
}
export const CustomAlert = ({
visible,
title,
message,
onClose,
actions = [
{ label: 'OK', onPress: onClose }
],
}: CustomAlertProps) => {
const opacity = useSharedValue(0);
const scale = useSharedValue(0.95);
const isDarkMode = useColorScheme() === 'dark';
const { currentTheme } = useTheme();
const themeColors = currentTheme.colors;
useEffect(() => {
const animDuration = 120;
if (visible) {
opacity.value = withTiming(1, { duration: animDuration });
scale.value = withTiming(1, { duration: animDuration });
} else {
opacity.value = withTiming(0, { duration: animDuration });
scale.value = withTiming(0.95, { duration: animDuration });
}
}, [visible]);
const overlayStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
const alertStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
const backgroundColor = isDarkMode ? themeColors.darkBackground : themeColors.elevation2 || '#FFFFFF';
const textColor = isDarkMode ? themeColors.white : themeColors.black || '#000000';
const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
return (
<Modal
visible={visible}
transparent
animationType="none"
onRequestClose={onClose}
>
<Animated.View style={[styles.overlay, { backgroundColor: themeColors.transparentDark }, overlayStyle]}>
<Pressable style={styles.overlayPressable} onPress={onClose} />
<View style={styles.centered}>
<Animated.View style={[styles.alertContainer, alertStyle, { backgroundColor, borderColor }]}>
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
<Text style={[styles.message, { color: textColor }]}>{message}</Text>
<View style={styles.actionsRow}>
{actions.map((action, idx) => (
<TouchableOpacity
key={action.label}
style={[styles.actionButton, idx === actions.length - 1 && styles.lastActionButton, action.style]}
onPress={() => {
action.onPress();
onClose();
}}
>
<Text style={[styles.actionText, { color: themeColors.primary }]}>{action.label}</Text>
</TouchableOpacity>
))}
</View>
</Animated.View>
</View>
</Animated.View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
overlayPressable: {
...StyleSheet.absoluteFillObject,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
alertContainer: {
minWidth: 280,
maxWidth: '85%',
borderRadius: 20,
padding: 24,
borderWidth: 1,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 8,
},
android: {
elevation: 8,
},
}),
},
title: {
fontSize: 18,
fontWeight: '700',
marginBottom: 12,
textAlign: 'center',
},
message: {
fontSize: 16,
marginBottom: 20,
textAlign: 'center',
},
actionsRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 12,
},
actionButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
},
lastActionButton: {
// Optionally style the last button differently
},
actionText: {
fontSize: 16,
fontWeight: '600',
},
});
export default CustomAlert;

View file

@ -7,7 +7,6 @@ import {
Dimensions,
AppState,
AppStateStatus,
Alert,
ActivityIndicator
} from 'react-native';
import { FlashList } from '@shopify/flash-list';
@ -24,6 +23,7 @@ import { logger } from '../../utils/logger';
import * as Haptics from 'expo-haptics';
import { TraktService } from '../../services/traktService';
import { stremioService } from '../../services/stremioService';
import CustomAlert from '../../components/CustomAlert';
// Define interface for continue watching items
interface ContinueWatchingItem extends StreamingContent {
@ -96,6 +96,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Alert state for CustomAlert
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
// Use a ref to track if a background refresh is in progress to avoid state updates
const isRefreshingRef = useRef(false);
@ -516,80 +522,51 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Ignore haptic errors
}
// Show confirmation alert
Alert.alert(
"Remove from Continue Watching",
`Remove "${item.name}" from your continue watching list?`,
[
{
text: "Cancel",
style: "cancel"
},
{
text: "Remove",
style: "destructive",
onPress: async () => {
setDeletingItemId(item.id);
try {
// Trigger haptic feedback for confirmation
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
// Remove all watch progress for this content (all episodes if series)
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
// Also remove from Trakt playback queue if authenticated
const traktService = TraktService.getInstance();
const isAuthed = await traktService.isAuthenticated();
logger.log(`🔍 [ContinueWatching] Trakt authentication status: ${isAuthed}`);
if (isAuthed) {
logger.log(`🗑️ [ContinueWatching] Removing Trakt history for ${item.id}`);
let traktResult = false;
if (item.type === 'movie') {
logger.log(`🎬 [ContinueWatching] Removing movie from Trakt history: ${item.name}`);
traktResult = await traktService.removeMovieFromHistory(item.id);
} else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) {
logger.log(`📺 [ContinueWatching] Removing specific episode from Trakt history: ${item.name} S${item.season}E${item.episode}`);
traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode);
} else {
logger.log(`📺 [ContinueWatching] Removing entire show from Trakt history: ${item.name} (no specific episode info)`);
traktResult = await traktService.removeShowFromHistory(item.id);
}
logger.log(`✅ [ContinueWatching] Trakt removal result: ${traktResult}`);
setAlertTitle('Remove from Continue Watching');
setAlertMessage(`Remove "${item.name}" from your continue watching list?`);
setAlertActions([
{
label: 'Cancel',
style: { color: '#888' },
onPress: () => {},
},
{
label: 'Remove',
style: { color: currentTheme.colors.error },
onPress: async () => {
setDeletingItemId(item.id);
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
const traktService = TraktService.getInstance();
const isAuthed = await traktService.isAuthenticated();
if (isAuthed) {
let traktResult = false;
if (item.type === 'movie') {
traktResult = await traktService.removeMovieFromHistory(item.id);
} else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) {
traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode);
} else {
logger.log(` [ContinueWatching] Skipping Trakt removal - not authenticated`);
traktResult = await traktService.removeShowFromHistory(item.id);
}
// Track this item as recently removed to prevent immediate re-addition
const itemKey = `${item.type}:${item.id}`;
recentlyRemovedRef.current.add(itemKey);
// Persist the removed state for long-term tracking
await storageService.addContinueWatchingRemoved(item.id, item.type);
// Clear from recently removed after the ignore duration
setTimeout(() => {
recentlyRemovedRef.current.delete(itemKey);
}, REMOVAL_IGNORE_DURATION);
// Update the list by filtering out the deleted item
setContinueWatchingItems(prev => {
const newList = prev.filter(i => i.id !== item.id);
return newList;
});
} catch (error) {
// Continue even if removal fails
} finally {
setDeletingItemId(null);
}
const itemKey = `${item.type}:${item.id}`;
recentlyRemovedRef.current.add(itemKey);
await storageService.addContinueWatchingRemoved(item.id, item.type);
setTimeout(() => {
recentlyRemovedRef.current.delete(itemKey);
}, REMOVAL_IGNORE_DURATION);
setContinueWatchingItems(prev => prev.filter(i => i.id !== item.id));
} catch (error) {
// Continue even if removal fails
} finally {
setDeletingItemId(null);
}
}
]
);
}, []);
},
},
]);
setAlertVisible(true);
}, [currentTheme.colors.error]);
// Memoized render function for continue watching items
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
@ -730,6 +707,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
onEndReached={() => {}}
removeClippedSubviews={true}
/>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</Animated.View>
);
});

View file

@ -7,7 +7,6 @@ import {
TextInput,
TouchableOpacity,
ActivityIndicator,
Alert,
SafeAreaView,
StatusBar,
Modal,
@ -30,6 +29,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
import { logger } from '../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { BlurView as ExpoBlurView } from 'expo-blur';
import CustomAlert from '../components/CustomAlert';
// Removed community blur and expo-constants for Android overlay
import axios from 'axios';
import { useTheme } from '../contexts/ThemeContext';
@ -597,6 +597,11 @@ const AddonsScreen = () => {
const [installing, setInstalling] = useState(false);
const [catalogCount, setCatalogCount] = useState(0);
// Add state for reorder mode
// Custom alert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
const [reorderMode, setReorderMode] = useState(false);
// Use ThemeContext
const { currentTheme } = useTheme();
@ -662,8 +667,11 @@ const AddonsScreen = () => {
setCatalogCount(totalCatalogs);
}
} catch (error) {
logger.error('Failed to load addons:', error);
Alert.alert('Error', 'Failed to load addons');
logger.error('Failed to load addons:', error);
setAlertTitle('Error');
setAlertMessage('Failed to load addons');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setLoading(false);
}
@ -683,10 +691,9 @@ const AddonsScreen = () => {
setCommunityAddons(validAddons);
} catch (error) {
logger.error('Failed to load community addons:', error);
setCommunityError('Failed to load community addons. Please try again later.');
// Set empty array on error since Cinemeta is pre-installed
setCommunityAddons([]);
logger.error('Failed to load community addons:', error);
setCommunityError('Failed to load community addons. Please try again later.');
setCommunityAddons([]);
} finally {
setCommunityLoading(false);
}
@ -695,7 +702,10 @@ const AddonsScreen = () => {
const handleAddAddon = async (url?: string) => {
const urlToInstall = url || addonUrl;
if (!urlToInstall) {
Alert.alert('Error', 'Please enter an addon URL or select a community addon');
setAlertTitle('Error');
setAlertMessage('Please enter an addon URL or select a community addon');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
@ -706,8 +716,11 @@ const AddonsScreen = () => {
setAddonUrl(urlToInstall);
setShowConfirmModal(true);
} catch (error) {
logger.error('Failed to fetch addon details:', error);
Alert.alert('Error', `Failed to fetch addon details from ${urlToInstall}`);
logger.error('Failed to fetch addon details:', error);
setAlertTitle('Error');
setAlertMessage(`Failed to fetch addon details from ${urlToInstall}`);
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setInstalling(false);
}
@ -723,10 +736,16 @@ const AddonsScreen = () => {
setShowConfirmModal(false);
setAddonDetails(null);
loadAddons();
Alert.alert('Success', 'Addon installed successfully');
setAlertTitle('Success');
setAlertMessage('Addon installed successfully');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Failed to install addon:', error);
Alert.alert('Error', 'Failed to install addon');
logger.error('Failed to install addon:', error);
setAlertTitle('Error');
setAlertMessage('Failed to install addon');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setInstalling(false);
}
@ -754,31 +773,26 @@ const AddonsScreen = () => {
const handleRemoveAddon = (addon: ExtendedManifest) => {
// Check if this is a pre-installed addon
if (stremioService.isPreInstalledAddon(addon.id)) {
Alert.alert(
'Cannot Remove Addon',
`${addon.name} is a pre-installed addon and cannot be removed.`,
[{ text: 'OK', style: 'default' }]
);
setAlertTitle('Cannot Remove Addon');
setAlertMessage(`${addon.name} is a pre-installed addon and cannot be removed.`);
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
Alert.alert(
'Uninstall Addon',
`Are you sure you want to uninstall ${addon.name}?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Uninstall',
style: 'destructive',
onPress: () => {
stremioService.removeAddon(addon.id);
// Remove from addons list
setAddons(prev => prev.filter(a => a.id !== addon.id));
},
setAlertTitle('Uninstall Addon');
setAlertMessage(`Are you sure you want to uninstall ${addon.name}?`);
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
label: 'Uninstall',
onPress: () => {
stremioService.removeAddon(addon.id);
setAddons(prev => prev.filter(a => a.id !== addon.id));
},
]
);
style: { color: colors.error }
},
]);
setAlertVisible(true);
};
// Add function to handle configuration
@ -876,11 +890,10 @@ const AddonsScreen = () => {
// If we couldn't determine a config URL, show an error
if (!configUrl) {
logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`);
Alert.alert(
'Configuration Unavailable',
'Could not determine configuration URL for this addon.',
[{ text: 'OK' }]
);
setAlertTitle('Configuration Unavailable');
setAlertMessage('Could not determine configuration URL for this addon.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
@ -893,15 +906,17 @@ const AddonsScreen = () => {
Linking.openURL(configUrl);
} else {
logger.error(`URL cannot be opened: ${configUrl}`);
Alert.alert(
'Cannot Open Configuration',
`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`,
[{ text: 'OK' }]
);
setAlertTitle('Cannot Open Configuration');
setAlertMessage(`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`);
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
}).catch(err => {
logger.error(`Error checking if URL can be opened: ${configUrl}`, err);
Alert.alert('Error', 'Could not open configuration page.');
setAlertTitle('Error');
setAlertMessage('Could not open configuration page.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
});
};
@ -1482,7 +1497,15 @@ const AddonsScreen = () => {
</View>
</View>
</Modal>
</SafeAreaView>
{/* Custom Alert Modal */}
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -11,9 +11,9 @@ import {
Platform,
useColorScheme,
ActivityIndicator,
Alert,
Animated
} from 'react-native';
import CustomAlert from '../components/CustomAlert';
import { useSettings, settingsEmitter } from '../hooks/useSettings';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
@ -35,6 +35,11 @@ const HeroCatalogsScreen: React.FC = () => {
const systemColorScheme = useColorScheme();
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
const navigation = useNavigation();
// Custom alert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
@ -120,7 +125,10 @@ const HeroCatalogsScreen: React.FC = () => {
setCatalogs(catalogItems);
} catch (error) {
if (__DEV__) console.error('Failed to load catalogs:', error);
Alert.alert('Error', 'Failed to load catalogs');
setAlertTitle('Error');
setAlertMessage('Failed to load catalogs');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setLoading(false);
}
@ -276,7 +284,14 @@ const HeroCatalogsScreen: React.FC = () => {
</ScrollView>
</>
)}
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -8,7 +8,6 @@ import {
SafeAreaView,
StatusBar,
Platform,
Alert,
ActivityIndicator,
Linking,
ScrollView,
@ -16,6 +15,7 @@ import {
Clipboard,
Switch,
} from 'react-native';
import CustomAlert from '../components/CustomAlert';
import { useNavigation } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import AsyncStorage from '@react-native-async-storage/async-storage';
@ -361,6 +361,11 @@ const MDBListSettingsScreen = () => {
const colors = currentTheme.colors;
const styles = createStyles(colors);
// Custom alert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
const [apiKey, setApiKey] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isKeySet, setIsKeySet] = useState(false);
@ -492,34 +497,32 @@ const MDBListSettingsScreen = () => {
const clearApiKey = async () => {
logger.log('[MDBListSettingsScreen] Clear API key requested');
Alert.alert(
'Clear API Key',
'Are you sure you want to remove the saved API key?',
[
{
text: 'Cancel',
style: 'cancel',
onPress: () => logger.log('[MDBListSettingsScreen] Clear API key cancelled')
},
{
text: 'Clear',
style: 'destructive',
onPress: async () => {
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
try {
await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY);
setApiKey('');
setIsKeySet(false);
setTestResult(null);
logger.log('[MDBListSettingsScreen] API key cleared successfully');
} catch (error) {
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
Alert.alert('Error', 'Failed to clear API key');
}
setAlertTitle('Clear API Key');
setAlertMessage('Are you sure you want to remove the saved API key?');
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
label: 'Clear',
onPress: async () => {
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
try {
await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY);
setApiKey('');
setIsKeySet(false);
setTestResult(null);
logger.log('[MDBListSettingsScreen] API key cleared successfully');
} catch (error) {
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
setAlertTitle('Error');
setAlertMessage('Failed to clear API key');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
}
]
);
},
style: { color: colors.error }
},
]);
setAlertVisible(true);
};
const pasteFromClipboard = async () => {
@ -823,7 +826,14 @@ const MDBListSettingsScreen = () => {
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -6,11 +6,11 @@ import {
ScrollView,
Switch,
TouchableOpacity,
Alert,
SafeAreaView,
StatusBar,
Platform,
} from 'react-native';
import CustomAlert from '../components/CustomAlert';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { notificationService, NotificationSettings } from '../services/notificationService';
@ -36,6 +36,11 @@ const NotificationSettingsScreen = () => {
const [isSyncing, setIsSyncing] = useState(false);
const [notificationStats, setNotificationStats] = useState({ total: 0, upcoming: 0, thisWeek: 0 });
// Custom alert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
// Load settings and stats on mount
useEffect(() => {
const loadSettings = async () => {
@ -104,7 +109,10 @@ const NotificationSettingsScreen = () => {
setSettings(updatedSettings);
} catch (error) {
logger.error('Error updating notification settings:', error);
Alert.alert('Error', 'Failed to update notification settings');
setAlertTitle('Error');
setAlertMessage('Failed to update notification settings');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
};
@ -114,33 +122,34 @@ const NotificationSettingsScreen = () => {
};
const resetAllNotifications = async () => {
Alert.alert(
'Reset Notifications',
'This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Reset',
style: 'destructive',
onPress: async () => {
try {
// Cancel all notifications for all series, but do not remove from saved
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
for (const notification of scheduledNotifications) {
await notificationService.cancelNotification(notification.id);
}
Alert.alert('Success', 'All notifications have been reset');
} catch (error) {
logger.error('Error resetting notifications:', error);
Alert.alert('Error', 'Failed to reset notifications');
setAlertTitle('Reset Notifications');
setAlertMessage('This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?');
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: currentTheme.colors.mediumGray } },
{
label: 'Reset',
onPress: async () => {
try {
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
for (const notification of scheduledNotifications) {
await notificationService.cancelNotification(notification.id);
}
},
setAlertTitle('Success');
setAlertMessage('All notifications have been reset');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Error resetting notifications:', error);
setAlertTitle('Error');
setAlertMessage('Failed to reset notifications');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
},
]
);
style: { color: currentTheme.colors.error }
},
]);
setAlertVisible(true);
};
const handleSyncNotifications = async () => {
@ -154,13 +163,16 @@ const NotificationSettingsScreen = () => {
const stats = notificationService.getNotificationStats();
setNotificationStats(stats);
Alert.alert(
'Sync Complete',
`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`
);
setAlertTitle('Sync Complete');
setAlertMessage(`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`);
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Error syncing notifications:', error);
Alert.alert('Error', 'Failed to sync notifications. Please try again.');
setAlertTitle('Error');
setAlertMessage('Failed to sync notifications. Please try again.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setIsSyncing(false);
}
@ -212,13 +224,22 @@ const NotificationSettingsScreen = () => {
if (notificationId) {
setTestNotificationId(notificationId);
setCountdown(0); // No countdown for instant notification
Alert.alert('Success', 'Test notification scheduled to fire instantly');
setAlertTitle('Success');
setAlertMessage('Test notification scheduled to fire instantly');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} else {
Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.');
setAlertTitle('Error');
setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
} catch (error) {
logger.error('Error scheduling test notification:', error);
Alert.alert('Error', 'Failed to schedule test notification');
setAlertTitle('Error');
setAlertMessage('Failed to schedule test notification');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
};
@ -475,7 +496,14 @@ const NotificationSettingsScreen = () => {
)}
</Animated.View>
</ScrollView>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

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