mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-26 19:12:54 +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==",
|
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
"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,
|
Dimensions,
|
||||||
AppState,
|
AppState,
|
||||||
AppStateStatus,
|
AppStateStatus,
|
||||||
Alert,
|
|
||||||
ActivityIndicator
|
ActivityIndicator
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { FlashList } from '@shopify/flash-list';
|
import { FlashList } from '@shopify/flash-list';
|
||||||
|
|
@ -24,6 +23,7 @@ import { logger } from '../../utils/logger';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { TraktService } from '../../services/traktService';
|
import { TraktService } from '../../services/traktService';
|
||||||
import { stremioService } from '../../services/stremioService';
|
import { stremioService } from '../../services/stremioService';
|
||||||
|
import CustomAlert from '../../components/CustomAlert';
|
||||||
|
|
||||||
// Define interface for continue watching items
|
// Define interface for continue watching items
|
||||||
interface ContinueWatchingItem extends StreamingContent {
|
interface ContinueWatchingItem extends StreamingContent {
|
||||||
|
|
@ -96,6 +96,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
|
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
|
||||||
const longPressTimeoutRef = useRef<NodeJS.Timeout | 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
|
// Use a ref to track if a background refresh is in progress to avoid state updates
|
||||||
const isRefreshingRef = useRef(false);
|
const isRefreshingRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -516,80 +522,51 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
// Ignore haptic errors
|
// Ignore haptic errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show confirmation alert
|
setAlertTitle('Remove from Continue Watching');
|
||||||
Alert.alert(
|
setAlertMessage(`Remove "${item.name}" from your continue watching list?`);
|
||||||
"Remove from Continue Watching",
|
setAlertActions([
|
||||||
`Remove "${item.name}" from your continue watching list?`,
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
text: "Cancel",
|
label: 'Cancel',
|
||||||
style: "cancel"
|
style: { color: '#888' },
|
||||||
|
onPress: () => {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Remove",
|
label: 'Remove',
|
||||||
style: "destructive",
|
style: { color: currentTheme.colors.error },
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
setDeletingItemId(item.id);
|
setDeletingItemId(item.id);
|
||||||
try {
|
try {
|
||||||
// Trigger haptic feedback for confirmation
|
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
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 });
|
await storageService.removeAllWatchProgressForContent(item.id, item.type, { addBaseTombstone: true });
|
||||||
|
|
||||||
// Also remove from Trakt playback queue if authenticated
|
|
||||||
const traktService = TraktService.getInstance();
|
const traktService = TraktService.getInstance();
|
||||||
const isAuthed = await traktService.isAuthenticated();
|
const isAuthed = await traktService.isAuthenticated();
|
||||||
logger.log(`🔍 [ContinueWatching] Trakt authentication status: ${isAuthed}`);
|
|
||||||
|
|
||||||
if (isAuthed) {
|
if (isAuthed) {
|
||||||
logger.log(`🗑️ [ContinueWatching] Removing Trakt history for ${item.id}`);
|
|
||||||
let traktResult = false;
|
let traktResult = false;
|
||||||
|
|
||||||
if (item.type === 'movie') {
|
if (item.type === 'movie') {
|
||||||
logger.log(`🎬 [ContinueWatching] Removing movie from Trakt history: ${item.name}`);
|
|
||||||
traktResult = await traktService.removeMovieFromHistory(item.id);
|
traktResult = await traktService.removeMovieFromHistory(item.id);
|
||||||
} else if (item.type === 'series' && item.season !== undefined && item.episode !== undefined) {
|
} 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);
|
traktResult = await traktService.removeEpisodeFromHistory(item.id, item.season, item.episode);
|
||||||
} else {
|
} else {
|
||||||
logger.log(`📺 [ContinueWatching] Removing entire show from Trakt history: ${item.name} (no specific episode info)`);
|
|
||||||
traktResult = await traktService.removeShowFromHistory(item.id);
|
traktResult = await traktService.removeShowFromHistory(item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`✅ [ContinueWatching] Trakt removal result: ${traktResult}`);
|
|
||||||
} else {
|
|
||||||
logger.log(`ℹ️ [ContinueWatching] Skipping Trakt removal - not authenticated`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track this item as recently removed to prevent immediate re-addition
|
|
||||||
const itemKey = `${item.type}:${item.id}`;
|
const itemKey = `${item.type}:${item.id}`;
|
||||||
recentlyRemovedRef.current.add(itemKey);
|
recentlyRemovedRef.current.add(itemKey);
|
||||||
|
|
||||||
// Persist the removed state for long-term tracking
|
|
||||||
await storageService.addContinueWatchingRemoved(item.id, item.type);
|
await storageService.addContinueWatchingRemoved(item.id, item.type);
|
||||||
|
|
||||||
// Clear from recently removed after the ignore duration
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
recentlyRemovedRef.current.delete(itemKey);
|
recentlyRemovedRef.current.delete(itemKey);
|
||||||
}, REMOVAL_IGNORE_DURATION);
|
}, REMOVAL_IGNORE_DURATION);
|
||||||
|
setContinueWatchingItems(prev => prev.filter(i => i.id !== item.id));
|
||||||
// Update the list by filtering out the deleted item
|
|
||||||
setContinueWatchingItems(prev => {
|
|
||||||
const newList = prev.filter(i => i.id !== item.id);
|
|
||||||
return newList;
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Continue even if removal fails
|
// Continue even if removal fails
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingItemId(null);
|
setDeletingItemId(null);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]);
|
||||||
);
|
setAlertVisible(true);
|
||||||
}, []);
|
}, [currentTheme.colors.error]);
|
||||||
|
|
||||||
// Memoized render function for continue watching items
|
// Memoized render function for continue watching items
|
||||||
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
|
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
|
||||||
|
|
@ -730,6 +707,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
onEndReached={() => {}}
|
onEndReached={() => {}}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
actions={alertActions}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Modal,
|
Modal,
|
||||||
|
|
@ -30,6 +29,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||||
|
import CustomAlert from '../components/CustomAlert';
|
||||||
// Removed community blur and expo-constants for Android overlay
|
// Removed community blur and expo-constants for Android overlay
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
@ -597,6 +597,11 @@ const AddonsScreen = () => {
|
||||||
const [installing, setInstalling] = useState(false);
|
const [installing, setInstalling] = useState(false);
|
||||||
const [catalogCount, setCatalogCount] = useState(0);
|
const [catalogCount, setCatalogCount] = useState(0);
|
||||||
// Add state for reorder mode
|
// 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);
|
const [reorderMode, setReorderMode] = useState(false);
|
||||||
// Use ThemeContext
|
// Use ThemeContext
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
@ -663,7 +668,10 @@ const AddonsScreen = () => {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load addons:', error);
|
logger.error('Failed to load addons:', error);
|
||||||
Alert.alert('Error', 'Failed to load addons');
|
setAlertTitle('Error');
|
||||||
|
setAlertMessage('Failed to load addons');
|
||||||
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
|
setAlertVisible(true);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -685,7 +693,6 @@ const AddonsScreen = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load community addons:', error);
|
logger.error('Failed to load community addons:', error);
|
||||||
setCommunityError('Failed to load community addons. Please try again later.');
|
setCommunityError('Failed to load community addons. Please try again later.');
|
||||||
// Set empty array on error since Cinemeta is pre-installed
|
|
||||||
setCommunityAddons([]);
|
setCommunityAddons([]);
|
||||||
} finally {
|
} finally {
|
||||||
setCommunityLoading(false);
|
setCommunityLoading(false);
|
||||||
|
|
@ -695,7 +702,10 @@ const AddonsScreen = () => {
|
||||||
const handleAddAddon = async (url?: string) => {
|
const handleAddAddon = async (url?: string) => {
|
||||||
const urlToInstall = url || addonUrl;
|
const urlToInstall = url || addonUrl;
|
||||||
if (!urlToInstall) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -707,7 +717,10 @@ const AddonsScreen = () => {
|
||||||
setShowConfirmModal(true);
|
setShowConfirmModal(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to fetch addon details:', error);
|
logger.error('Failed to fetch addon details:', error);
|
||||||
Alert.alert('Error', `Failed to fetch addon details from ${urlToInstall}`);
|
setAlertTitle('Error');
|
||||||
|
setAlertMessage(`Failed to fetch addon details from ${urlToInstall}`);
|
||||||
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
|
setAlertVisible(true);
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
}
|
}
|
||||||
|
|
@ -723,10 +736,16 @@ const AddonsScreen = () => {
|
||||||
setShowConfirmModal(false);
|
setShowConfirmModal(false);
|
||||||
setAddonDetails(null);
|
setAddonDetails(null);
|
||||||
loadAddons();
|
loadAddons();
|
||||||
Alert.alert('Success', 'Addon installed successfully');
|
setAlertTitle('Success');
|
||||||
|
setAlertMessage('Addon installed successfully');
|
||||||
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
|
setAlertVisible(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to install addon:', error);
|
logger.error('Failed to install addon:', error);
|
||||||
Alert.alert('Error', 'Failed to install addon');
|
setAlertTitle('Error');
|
||||||
|
setAlertMessage('Failed to install addon');
|
||||||
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
|
setAlertVisible(true);
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
}
|
}
|
||||||
|
|
@ -754,31 +773,26 @@ const AddonsScreen = () => {
|
||||||
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
const handleRemoveAddon = (addon: ExtendedManifest) => {
|
||||||
// Check if this is a pre-installed addon
|
// Check if this is a pre-installed addon
|
||||||
if (stremioService.isPreInstalledAddon(addon.id)) {
|
if (stremioService.isPreInstalledAddon(addon.id)) {
|
||||||
Alert.alert(
|
setAlertTitle('Cannot Remove Addon');
|
||||||
'Cannot Remove Addon',
|
setAlertMessage(`${addon.name} is a pre-installed addon and cannot be removed.`);
|
||||||
`${addon.name} is a pre-installed addon and cannot be removed.`,
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
[{ text: 'OK', style: 'default' }]
|
setAlertVisible(true);
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setAlertTitle('Uninstall Addon');
|
||||||
Alert.alert(
|
setAlertMessage(`Are you sure you want to uninstall ${addon.name}?`);
|
||||||
'Uninstall Addon',
|
setAlertActions([
|
||||||
`Are you sure you want to uninstall ${addon.name}?`,
|
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
|
||||||
[
|
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
|
||||||
{
|
{
|
||||||
text: 'Uninstall',
|
label: 'Uninstall',
|
||||||
style: 'destructive',
|
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
stremioService.removeAddon(addon.id);
|
stremioService.removeAddon(addon.id);
|
||||||
|
|
||||||
// Remove from addons list
|
|
||||||
setAddons(prev => prev.filter(a => a.id !== addon.id));
|
setAddons(prev => prev.filter(a => a.id !== addon.id));
|
||||||
},
|
},
|
||||||
|
style: { color: colors.error }
|
||||||
},
|
},
|
||||||
]
|
]);
|
||||||
);
|
setAlertVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add function to handle configuration
|
// Add function to handle configuration
|
||||||
|
|
@ -876,11 +890,10 @@ const AddonsScreen = () => {
|
||||||
// If we couldn't determine a config URL, show an error
|
// If we couldn't determine a config URL, show an error
|
||||||
if (!configUrl) {
|
if (!configUrl) {
|
||||||
logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`);
|
logger.error(`Failed to determine config URL for addon: ${addon.name}, ID: ${addon.id}`);
|
||||||
Alert.alert(
|
setAlertTitle('Configuration Unavailable');
|
||||||
'Configuration Unavailable',
|
setAlertMessage('Could not determine configuration URL for this addon.');
|
||||||
'Could not determine configuration URL for this addon.',
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
[{ text: 'OK' }]
|
setAlertVisible(true);
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -893,15 +906,17 @@ const AddonsScreen = () => {
|
||||||
Linking.openURL(configUrl);
|
Linking.openURL(configUrl);
|
||||||
} else {
|
} else {
|
||||||
logger.error(`URL cannot be opened: ${configUrl}`);
|
logger.error(`URL cannot be opened: ${configUrl}`);
|
||||||
Alert.alert(
|
setAlertTitle('Cannot Open Configuration');
|
||||||
'Cannot Open Configuration',
|
setAlertMessage(`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`);
|
||||||
`The configuration URL (${configUrl}) cannot be opened. The addon may not have a configuration page.`,
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
[{ text: 'OK' }]
|
setAlertVisible(true);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
logger.error(`Error checking if URL can be opened: ${configUrl}`, 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,6 +1497,14 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
{/* Custom Alert Modal */}
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
actions={alertActions}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import {
|
||||||
Platform,
|
Platform,
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
|
||||||
Animated
|
Animated
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import CustomAlert from '../components/CustomAlert';
|
||||||
import { useSettings, settingsEmitter } from '../hooks/useSettings';
|
import { useSettings, settingsEmitter } from '../hooks/useSettings';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
|
@ -35,6 +35,11 @@ const HeroCatalogsScreen: React.FC = () => {
|
||||||
const systemColorScheme = useColorScheme();
|
const systemColorScheme = useColorScheme();
|
||||||
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
|
const isDarkMode = systemColorScheme === 'dark' || settings.enableDarkMode;
|
||||||
const navigation = useNavigation();
|
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 [loading, setLoading] = useState(true);
|
||||||
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
|
const [catalogs, setCatalogs] = useState<CatalogItem[]>([]);
|
||||||
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
|
const [selectedCatalogs, setSelectedCatalogs] = useState<string[]>(settings.selectedHeroCatalogs || []);
|
||||||
|
|
@ -120,7 +125,10 @@ const HeroCatalogsScreen: React.FC = () => {
|
||||||
setCatalogs(catalogItems);
|
setCatalogs(catalogItems);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (__DEV__) console.error('Failed to load catalogs:', 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -276,6 +284,13 @@ const HeroCatalogsScreen: React.FC = () => {
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
actions={alertActions}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import {
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
Alert,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Linking,
|
Linking,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
|
@ -16,6 +15,7 @@ import {
|
||||||
Clipboard,
|
Clipboard,
|
||||||
Switch,
|
Switch,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import CustomAlert from '../components/CustomAlert';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
@ -361,6 +361,11 @@ const MDBListSettingsScreen = () => {
|
||||||
const colors = currentTheme.colors;
|
const colors = currentTheme.colors;
|
||||||
const styles = createStyles(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 [apiKey, setApiKey] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isKeySet, setIsKeySet] = useState(false);
|
const [isKeySet, setIsKeySet] = useState(false);
|
||||||
|
|
@ -492,18 +497,12 @@ const MDBListSettingsScreen = () => {
|
||||||
|
|
||||||
const clearApiKey = async () => {
|
const clearApiKey = async () => {
|
||||||
logger.log('[MDBListSettingsScreen] Clear API key requested');
|
logger.log('[MDBListSettingsScreen] Clear API key requested');
|
||||||
Alert.alert(
|
setAlertTitle('Clear API Key');
|
||||||
'Clear API Key',
|
setAlertMessage('Are you sure you want to remove the saved API key?');
|
||||||
'Are you sure you want to remove the saved API key?',
|
setAlertActions([
|
||||||
[
|
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
label: 'Clear',
|
||||||
style: 'cancel',
|
|
||||||
onPress: () => logger.log('[MDBListSettingsScreen] Clear API key cancelled')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Clear',
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
|
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
|
||||||
try {
|
try {
|
||||||
|
|
@ -514,12 +513,16 @@ const MDBListSettingsScreen = () => {
|
||||||
logger.log('[MDBListSettingsScreen] API key cleared successfully');
|
logger.log('[MDBListSettingsScreen] API key cleared successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
|
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
|
||||||
Alert.alert('Error', 'Failed to clear API key');
|
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 () => {
|
const pasteFromClipboard = async () => {
|
||||||
|
|
@ -823,6 +826,13 @@ const MDBListSettingsScreen = () => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
actions={alertActions}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Switch,
|
Switch,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import CustomAlert from '../components/CustomAlert';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { notificationService, NotificationSettings } from '../services/notificationService';
|
import { notificationService, NotificationSettings } from '../services/notificationService';
|
||||||
|
|
@ -36,6 +36,11 @@ const NotificationSettingsScreen = () => {
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [notificationStats, setNotificationStats] = useState({ total: 0, upcoming: 0, thisWeek: 0 });
|
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
|
// Load settings and stats on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
|
|
@ -104,7 +109,10 @@ const NotificationSettingsScreen = () => {
|
||||||
setSettings(updatedSettings);
|
setSettings(updatedSettings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error updating notification settings:', 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 () => {
|
const resetAllNotifications = async () => {
|
||||||
Alert.alert(
|
setAlertTitle('Reset Notifications');
|
||||||
'Reset Notifications',
|
setAlertMessage('This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?');
|
||||||
'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 } },
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
label: 'Reset',
|
||||||
style: 'cancel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Reset',
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
try {
|
try {
|
||||||
// Cancel all notifications for all series, but do not remove from saved
|
|
||||||
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
|
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
|
||||||
for (const notification of scheduledNotifications) {
|
for (const notification of scheduledNotifications) {
|
||||||
await notificationService.cancelNotification(notification.id);
|
await notificationService.cancelNotification(notification.id);
|
||||||
}
|
}
|
||||||
Alert.alert('Success', 'All notifications have been reset');
|
setAlertTitle('Success');
|
||||||
|
setAlertMessage('All notifications have been reset');
|
||||||
|
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
|
||||||
|
setAlertVisible(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error resetting notifications:', error);
|
logger.error('Error resetting notifications:', error);
|
||||||
Alert.alert('Error', 'Failed to reset notifications');
|
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 () => {
|
const handleSyncNotifications = async () => {
|
||||||
|
|
@ -154,13 +163,16 @@ const NotificationSettingsScreen = () => {
|
||||||
const stats = notificationService.getNotificationStats();
|
const stats = notificationService.getNotificationStats();
|
||||||
setNotificationStats(stats);
|
setNotificationStats(stats);
|
||||||
|
|
||||||
Alert.alert(
|
setAlertTitle('Sync Complete');
|
||||||
'Sync Complete',
|
setAlertMessage(`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`);
|
||||||
`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) {
|
} catch (error) {
|
||||||
logger.error('Error syncing notifications:', 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 {
|
} finally {
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
}
|
}
|
||||||
|
|
@ -212,13 +224,22 @@ const NotificationSettingsScreen = () => {
|
||||||
if (notificationId) {
|
if (notificationId) {
|
||||||
setTestNotificationId(notificationId);
|
setTestNotificationId(notificationId);
|
||||||
setCountdown(0); // No countdown for instant notification
|
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 {
|
} 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) {
|
} catch (error) {
|
||||||
logger.error('Error scheduling test notification:', 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,6 +496,13 @@ const NotificationSettingsScreen = () => {
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
actions={alertActions}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Switch,
|
Switch,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Alert,
|
|
||||||
Platform,
|
Platform,
|
||||||
TextInput,
|
TextInput,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
|
|
@ -23,6 +22,7 @@ import { colors } from '../styles/colors';
|
||||||
import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
|
import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -153,10 +153,20 @@ interface ThemeColorEditorProps {
|
||||||
onCancel: () => void;
|
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,
|
initialColors,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel
|
onCancel,
|
||||||
|
setAlertTitle,
|
||||||
|
setAlertMessage,
|
||||||
|
setAlertActions,
|
||||||
|
setAlertVisible
|
||||||
}) => {
|
}) => {
|
||||||
const [themeName, setThemeName] = useState('Custom Theme');
|
const [themeName, setThemeName] = useState('Custom Theme');
|
||||||
const [selectedColorKey, setSelectedColorKey] = useState<ColorKey>('primary');
|
const [selectedColorKey, setSelectedColorKey] = useState<ColorKey>('primary');
|
||||||
|
|
@ -175,7 +185,10 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps> = ({
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!themeName.trim()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
onSave({
|
onSave({
|
||||||
|
|
@ -318,6 +331,10 @@ const ThemeScreen: React.FC = () => {
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const [editingTheme, setEditingTheme] = useState<Theme | null>(null);
|
const [editingTheme, setEditingTheme] = useState<Theme | null>(null);
|
||||||
const [activeFilter, setActiveFilter] = useState('all');
|
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
|
// Force consistent status bar settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -373,19 +390,18 @@ const ThemeScreen: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDeleteTheme = useCallback((theme: Theme) => {
|
const handleDeleteTheme = useCallback((theme: Theme) => {
|
||||||
Alert.alert(
|
setAlertTitle('Delete Theme');
|
||||||
'Delete Theme',
|
setAlertMessage(`Are you sure you want to delete "${theme.name}"?`);
|
||||||
`Are you sure you want to delete "${theme.name}"?`,
|
setAlertActions([
|
||||||
[
|
{ label: 'Cancel', style: { color: '#888' }, onPress: () => {} },
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
|
||||||
{
|
{
|
||||||
text: 'Delete',
|
label: 'Delete',
|
||||||
style: 'destructive',
|
style: { color: currentTheme.colors.error },
|
||||||
onPress: () => deleteCustomTheme(theme.id)
|
onPress: () => deleteCustomTheme(theme.id),
|
||||||
}
|
},
|
||||||
]
|
]);
|
||||||
);
|
setAlertVisible(true);
|
||||||
}, [deleteCustomTheme]);
|
}, [deleteCustomTheme, currentTheme.colors.error]);
|
||||||
|
|
||||||
const handleCreateTheme = useCallback(() => {
|
const handleCreateTheme = useCallback(() => {
|
||||||
setEditingTheme(null);
|
setEditingTheme(null);
|
||||||
|
|
@ -427,6 +443,33 @@ const ThemeScreen: React.FC = () => {
|
||||||
setEditingTheme(null);
|
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) {
|
if (isEditMode) {
|
||||||
const initialColors = editingTheme ? {
|
const initialColors = editingTheme ? {
|
||||||
primary: editingTheme.colors.primary,
|
primary: editingTheme.colors.primary,
|
||||||
|
|
@ -441,15 +484,24 @@ const ThemeScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[
|
<SafeAreaView style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{
|
{ backgroundColor: currentTheme.colors.darkBackground }
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
|
||||||
}
|
|
||||||
]}>
|
]}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
<ThemeColorEditor
|
<ThemeColorEditor
|
||||||
initialColors={initialColors}
|
initialColors={initialColors}
|
||||||
onSave={handleSaveTheme}
|
onSave={handleSaveTheme}
|
||||||
onCancel={handleCancelEdit}
|
onCancel={handleCancelEdit}
|
||||||
|
setAlertTitle={setAlertTitle}
|
||||||
|
setAlertMessage={setAlertMessage}
|
||||||
|
setAlertActions={setAlertActions}
|
||||||
|
setAlertVisible={setAlertVisible}
|
||||||
|
/>
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
actions={alertActions}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
@ -458,9 +510,7 @@ const ThemeScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[
|
<SafeAreaView style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
{
|
{ backgroundColor: currentTheme.colors.darkBackground }
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
|
||||||
}
|
|
||||||
]}>
|
]}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
|
|
@ -553,6 +603,14 @@ const ThemeScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<CustomAlert
|
||||||
|
visible={alertVisible}
|
||||||
|
title={alertTitle}
|
||||||
|
message={alertMessage}
|
||||||
|
actions={alertActions}
|
||||||
|
onClose={() => setAlertVisible(false)}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue