mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Updated confirmation popup to be more custom and uses the current selected theme
This commit is contained in:
parent
f961e5ac3f
commit
8d60bff989
8 changed files with 496 additions and 222 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
166
src/components/CustomAlert.tsx
Normal file
166
src/components/CustomAlert.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
interface CustomAlertProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
actions?: Array<{
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
style?: object;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const CustomAlert = ({
|
||||
visible,
|
||||
title,
|
||||
message,
|
||||
onClose,
|
||||
actions = [
|
||||
{ label: 'OK', onPress: onClose }
|
||||
],
|
||||
}: CustomAlertProps) => {
|
||||
const opacity = useSharedValue(0);
|
||||
const scale = useSharedValue(0.95);
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
const { currentTheme } = useTheme();
|
||||
const themeColors = currentTheme.colors;
|
||||
|
||||
useEffect(() => {
|
||||
const animDuration = 120;
|
||||
if (visible) {
|
||||
opacity.value = withTiming(1, { duration: animDuration });
|
||||
scale.value = withTiming(1, { duration: animDuration });
|
||||
} else {
|
||||
opacity.value = withTiming(0, { duration: animDuration });
|
||||
scale.value = withTiming(0.95, { duration: animDuration });
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const overlayStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
}));
|
||||
|
||||
const alertStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
opacity: opacity.value,
|
||||
}));
|
||||
|
||||
const backgroundColor = isDarkMode ? themeColors.darkBackground : themeColors.elevation2 || '#FFFFFF';
|
||||
const textColor = isDarkMode ? themeColors.white : themeColors.black || '#000000';
|
||||
const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="none"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Animated.View style={[styles.overlay, { backgroundColor: themeColors.transparentDark }, overlayStyle]}>
|
||||
<Pressable style={styles.overlayPressable} onPress={onClose} />
|
||||
<View style={styles.centered}>
|
||||
<Animated.View style={[styles.alertContainer, alertStyle, { backgroundColor, borderColor }]}>
|
||||
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
|
||||
<Text style={[styles.message, { color: textColor }]}>{message}</Text>
|
||||
<View style={styles.actionsRow}>
|
||||
{actions.map((action, idx) => (
|
||||
<TouchableOpacity
|
||||
key={action.label}
|
||||
style={[styles.actionButton, idx === actions.length - 1 && styles.lastActionButton, action.style]}
|
||||
onPress={() => {
|
||||
action.onPress();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.actionText, { color: themeColors.primary }]}>{action.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlayPressable: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
alertContainer: {
|
||||
minWidth: 280,
|
||||
maxWidth: '85%',
|
||||
borderRadius: 20,
|
||||
padding: 24,
|
||||
borderWidth: 1,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
},
|
||||
android: {
|
||||
elevation: 8,
|
||||
},
|
||||
}),
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
message: {
|
||||
fontSize: 16,
|
||||
marginBottom: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
actionsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
},
|
||||
actionButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
},
|
||||
lastActionButton: {
|
||||
// Optionally style the last button differently
|
||||
},
|
||||
actionText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default CustomAlert;
|
||||
|
|
@ -7,7 +7,6 @@ import {
|
|||
Dimensions,
|
||||
AppState,
|
||||
AppStateStatus,
|
||||
Alert,
|
||||
ActivityIndicator
|
||||
} from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
|
|
@ -24,6 +23,7 @@ import { logger } from '../../utils/logger';
|
|||
import * as Haptics from 'expo-haptics';
|
||||
import { TraktService } from '../../services/traktService';
|
||||
import { stremioService } from '../../services/stremioService';
|
||||
import CustomAlert from '../../components/CustomAlert';
|
||||
|
||||
// Define interface for continue watching items
|
||||
interface ContinueWatchingItem extends StreamingContent {
|
||||
|
|
@ -96,6 +96,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
|
||||
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Alert state for CustomAlert
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
const [alertMessage, setAlertMessage] = useState('');
|
||||
const [alertActions, setAlertActions] = useState<any[]>([]);
|
||||
|
||||
// Use a ref to track if a background refresh is in progress to avoid state updates
|
||||
const isRefreshingRef = useRef(false);
|
||||
|
||||
|
|
@ -516,80 +522,51 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
// Ignore haptic errors
|
||||
}
|
||||
|
||||
// Show confirmation alert
|
||||
Alert.alert(
|
||||
"Remove from Continue Watching",
|
||||
`Remove "${item.name}" from your continue watching list?`,
|
||||
[
|
||||
{
|
||||
text: "Cancel",
|
||||
style: "cancel"
|
||||
},
|
||||
{
|
||||
text: "Remove",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
setDeletingItemId(item.id);
|
||||
try {
|
||||
// Trigger haptic feedback for confirmation
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
// Remove all watch progress for this content (all episodes if series)
|
||||
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
|
||||
|
||||
// Also remove from Trakt playback queue if authenticated
|
||||
const traktService = TraktService.getInstance();
|
||||
const isAuthed = await traktService.isAuthenticated();
|
||||
logger.log(`🔍 [ContinueWatching] Trakt authentication status: ${isAuthed}`);
|
||||
|
||||
if (isAuthed) {
|
||||
logger.log(`🗑️ [ContinueWatching] Removing Trakt history for ${item.id}`);
|
||||
let traktResult = false;
|
||||
|
||||
if (item.type === 'movie') {
|
||||
logger.log(`🎬 [ContinueWatching] Removing movie from Trakt history: ${item.name}`);
|
||||
traktResult = await traktService.removeMovieFromHistory(item.id);
|
||||
} else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) {
|
||||
logger.log(`📺 [ContinueWatching] Removing specific episode from Trakt history: ${item.name} S${item.season}E${item.episode}`);
|
||||
traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode);
|
||||
} else {
|
||||
logger.log(`📺 [ContinueWatching] Removing entire show from Trakt history: ${item.name} (no specific episode info)`);
|
||||
traktResult = await traktService.removeShowFromHistory(item.id);
|
||||
}
|
||||
|
||||
logger.log(`✅ [ContinueWatching] Trakt removal result: ${traktResult}`);
|
||||
setAlertTitle('Remove from Continue Watching');
|
||||
setAlertMessage(`Remove "${item.name}" from your continue watching list?`);
|
||||
setAlertActions([
|
||||
{
|
||||
label: 'Cancel',
|
||||
style: { color: '#888' },
|
||||
onPress: () => {},
|
||||
},
|
||||
{
|
||||
label: 'Remove',
|
||||
style: { color: currentTheme.colors.error },
|
||||
onPress: async () => {
|
||||
setDeletingItemId(item.id);
|
||||
try {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
|
||||
const traktService = TraktService.getInstance();
|
||||
const isAuthed = await traktService.isAuthenticated();
|
||||
if (isAuthed) {
|
||||
let traktResult = false;
|
||||
if (item.type === 'movie') {
|
||||
traktResult = await traktService.removeMovieFromHistory(item.id);
|
||||
} else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) {
|
||||
traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode);
|
||||
} else {
|
||||
logger.log(`ℹ️ [ContinueWatching] Skipping Trakt removal - not authenticated`);
|
||||
traktResult = await traktService.removeShowFromHistory(item.id);
|
||||
}
|
||||
|
||||
// Track this item as recently removed to prevent immediate re-addition
|
||||
const itemKey = `${item.type}:${item.id}`;
|
||||
recentlyRemovedRef.current.add(itemKey);
|
||||
|
||||
// Persist the removed state for long-term tracking
|
||||
await storageService.addContinueWatchingRemoved(item.id, item.type);
|
||||
|
||||
// Clear from recently removed after the ignore duration
|
||||
setTimeout(() => {
|
||||
recentlyRemovedRef.current.delete(itemKey);
|
||||
}, REMOVAL_IGNORE_DURATION);
|
||||
|
||||
// Update the list by filtering out the deleted item
|
||||
setContinueWatchingItems(prev => {
|
||||
const newList = prev.filter(i => i.id !== item.id);
|
||||
return newList;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Continue even if removal fails
|
||||
} finally {
|
||||
setDeletingItemId(null);
|
||||
}
|
||||
const itemKey = `${item.type}:${item.id}`;
|
||||
recentlyRemovedRef.current.add(itemKey);
|
||||
await storageService.addContinueWatchingRemoved(item.id, item.type);
|
||||
setTimeout(() => {
|
||||
recentlyRemovedRef.current.delete(itemKey);
|
||||
}, REMOVAL_IGNORE_DURATION);
|
||||
setContinueWatchingItems(prev => prev.filter(i => i.id !== item.id));
|
||||
} catch (error) {
|
||||
// Continue even if removal fails
|
||||
} finally {
|
||||
setDeletingItemId(null);
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
]);
|
||||
setAlertVisible(true);
|
||||
}, [currentTheme.colors.error]);
|
||||
|
||||
// Memoized render function for continue watching items
|
||||
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
|
||||
|
|
@ -730,6 +707,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
onEndReached={() => {}}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
title={alertTitle}
|
||||
message={alertMessage}
|
||||
actions={alertActions}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import {
|
|||
Platform,
|
||||
useColorScheme,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated
|
||||
} from 'react-native';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { useSettings, settingsEmitter } from '../hooks/useSettings';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
|
@ -35,6 +35,11 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
const systemColorScheme = useColorScheme();
|
||||
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
|
||||
const navigation = useNavigation();
|
||||
// Custom alert state
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
const [alertMessage, setAlertMessage] = useState('');
|
||||
const [alertActions, setAlertActions] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
|
||||
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
|
||||
|
|
@ -120,7 +125,10 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
setCatalogs(catalogItems);
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Failed to load catalogs:', error);
|
||||
Alert.alert('Error', 'Failed to load catalogs');
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to load catalogs');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -276,7 +284,14 @@ const HeroCatalogsScreen: React.FC = () => {
|
|||
</ScrollView>
|
||||
</>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
title={alertTitle}
|
||||
message={alertMessage}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
SafeAreaView,
|
||||
StatusBar,
|
||||
Platform,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
ScrollView,
|
||||
|
|
@ -16,6 +15,7 @@ import {
|
|||
Clipboard,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
|
@ -361,6 +361,11 @@ const MDBListSettingsScreen = () => {
|
|||
const colors = currentTheme.colors;
|
||||
const styles = createStyles(colors);
|
||||
|
||||
// Custom alert state
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
const [alertMessage, setAlertMessage] = useState('');
|
||||
const [alertActions, setAlertActions] = useState<any[]>([]);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isKeySet, setIsKeySet] = useState(false);
|
||||
|
|
@ -492,34 +497,32 @@ const MDBListSettingsScreen = () => {
|
|||
|
||||
const clearApiKey = async () => {
|
||||
logger.log('[MDBListSettingsScreen] Clear API key requested');
|
||||
Alert.alert(
|
||||
'Clear API Key',
|
||||
'Are you sure you want to remove the saved API key?',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
onPress: () => logger.log('[MDBListSettingsScreen] Clear API key cancelled')
|
||||
},
|
||||
{
|
||||
text: 'Clear',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
|
||||
try {
|
||||
await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY);
|
||||
setApiKey('');
|
||||
setIsKeySet(false);
|
||||
setTestResult(null);
|
||||
logger.log('[MDBListSettingsScreen] API key cleared successfully');
|
||||
} catch (error) {
|
||||
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
|
||||
Alert.alert('Error', 'Failed to clear API key');
|
||||
}
|
||||
setAlertTitle('Clear API Key');
|
||||
setAlertMessage('Are you sure you want to remove the saved API key?');
|
||||
setAlertActions([
|
||||
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
|
||||
{
|
||||
label: 'Clear',
|
||||
onPress: async () => {
|
||||
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
|
||||
try {
|
||||
await AsyncStorage.removeItem(MDBLIST_API_KEY_STORAGE_KEY);
|
||||
setApiKey('');
|
||||
setIsKeySet(false);
|
||||
setTestResult(null);
|
||||
logger.log('[MDBListSettingsScreen] API key cleared successfully');
|
||||
} catch (error) {
|
||||
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to clear API key');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
},
|
||||
style: { color: colors.error }
|
||||
},
|
||||
]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
const pasteFromClipboard = async () => {
|
||||
|
|
@ -823,7 +826,14 @@ const MDBListSettingsScreen = () => {
|
|||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
title={alertTitle}
|
||||
message={alertMessage}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import {
|
|||
ScrollView,
|
||||
Switch,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { notificationService, NotificationSettings } from '../services/notificationService';
|
||||
|
|
@ -36,6 +36,11 @@ const NotificationSettingsScreen = () => {
|
|||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [notificationStats, setNotificationStats] = useState({ total: 0, upcoming: 0, thisWeek: 0 });
|
||||
|
||||
// Custom alert state
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
const [alertMessage, setAlertMessage] = useState('');
|
||||
const [alertActions, setAlertActions] = useState<any[]>([]);
|
||||
// Load settings and stats on mount
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
|
|
@ -104,7 +109,10 @@ const NotificationSettingsScreen = () => {
|
|||
setSettings(updatedSettings);
|
||||
} catch (error) {
|
||||
logger.error('Error updating notification settings:', error);
|
||||
Alert.alert('Error', 'Failed to update notification settings');
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to update notification settings');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -114,33 +122,34 @@ const NotificationSettingsScreen = () => {
|
|||
};
|
||||
|
||||
const resetAllNotifications = async () => {
|
||||
Alert.alert(
|
||||
'Reset Notifications',
|
||||
'This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Reset',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
// Cancel all notifications for all series, but do not remove from saved
|
||||
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
|
||||
for (const notification of scheduledNotifications) {
|
||||
await notificationService.cancelNotification(notification.id);
|
||||
}
|
||||
Alert.alert('Success', 'All notifications have been reset');
|
||||
} catch (error) {
|
||||
logger.error('Error resetting notifications:', error);
|
||||
Alert.alert('Error', 'Failed to reset notifications');
|
||||
setAlertTitle('Reset Notifications');
|
||||
setAlertMessage('This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?');
|
||||
setAlertActions([
|
||||
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: currentTheme.colors.mediumGray } },
|
||||
{
|
||||
label: 'Reset',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
|
||||
for (const notification of scheduledNotifications) {
|
||||
await notificationService.cancelNotification(notification.id);
|
||||
}
|
||||
},
|
||||
setAlertTitle('Success');
|
||||
setAlertMessage('All notifications have been reset');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} catch (error) {
|
||||
logger.error('Error resetting notifications:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to reset notifications');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
},
|
||||
]
|
||||
);
|
||||
style: { color: currentTheme.colors.error }
|
||||
},
|
||||
]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
const handleSyncNotifications = async () => {
|
||||
|
|
@ -154,13 +163,16 @@ const NotificationSettingsScreen = () => {
|
|||
const stats = notificationService.getNotificationStats();
|
||||
setNotificationStats(stats);
|
||||
|
||||
Alert.alert(
|
||||
'Sync Complete',
|
||||
`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`
|
||||
);
|
||||
setAlertTitle('Sync Complete');
|
||||
setAlertMessage(`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`);
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} catch (error) {
|
||||
logger.error('Error syncing notifications:', error);
|
||||
Alert.alert('Error', 'Failed to sync notifications. Please try again.');
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to sync notifications. Please try again.');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
|
|
@ -212,13 +224,22 @@ const NotificationSettingsScreen = () => {
|
|||
if (notificationId) {
|
||||
setTestNotificationId(notificationId);
|
||||
setCountdown(0); // No countdown for instant notification
|
||||
Alert.alert('Success', 'Test notification scheduled to fire instantly');
|
||||
setAlertTitle('Success');
|
||||
setAlertMessage('Test notification scheduled to fire instantly');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to schedule test notification. Make sure notifications are enabled.');
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error scheduling test notification:', error);
|
||||
Alert.alert('Error', 'Failed to schedule test notification');
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Failed to schedule test notification');
|
||||
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -475,7 +496,14 @@ const NotificationSettingsScreen = () => {
|
|||
)}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
title={alertTitle}
|
||||
message={alertMessage}
|
||||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in a new issue