NuvioStreaming/src/screens/ProfilesScreen.tsx
tapframe 86d3035de6 tv focus highlight everywhere
Replace remaining screen touchables with focusable wrappers and add shared TV focus presets for consistent, visible focus rings across the app.
2025-12-26 19:07:29 +05:30

466 lines
No EOL
13 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
StatusBar,
Platform,
SafeAreaView,
TextInput,
Modal
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { useTraktContext } from '../contexts/TraktContext';
import { mmkvStorage } from '../services/mmkvStorage';
import CustomAlert from '../components/CustomAlert';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const PROFILE_STORAGE_KEY = 'user_profiles';
interface Profile {
id: string;
name: string;
avatar?: string;
isActive: boolean;
createdAt: number;
}
const ProfilesScreen: React.FC = () => {
const navigation = useNavigation();
const { currentTheme } = useTheme();
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
const [profiles, setProfiles] = useState<Profile[]>([]);
const [showAddModal, setShowAddModal] = useState(false);
const [newProfileName, setNewProfileName] = useState('');
const [isLoading, setIsLoading] = useState(true);
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([]);
const openAlert = (
title: string,
message: string,
actions?: Array<{ label: string; onPress: () => void; style?: object }>
) => {
setAlertTitle(title);
setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
setAlertVisible(true);
};
// Load profiles from AsyncStorage
const loadProfiles = useCallback(async () => {
try {
setIsLoading(true);
const storedProfiles = await mmkvStorage.getItem(PROFILE_STORAGE_KEY);
if (storedProfiles) {
setProfiles(JSON.parse(storedProfiles));
} else {
// If no profiles exist, create a default one with the Trakt username
const defaultProfile: Profile = {
id: new Date().getTime().toString(),
name: userProfile?.username || 'Default',
isActive: true,
createdAt: new Date().getTime()
};
setProfiles([defaultProfile]);
await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify([defaultProfile]));
}
} catch (error) {
if (__DEV__) console.error('Error loading profiles:', error);
openAlert('Error', 'Failed to load profiles');
} finally {
setIsLoading(false);
}
}, [userProfile]);
// Add a focus listener to refresh authentication status
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// Refresh the auth status when the screen comes into focus
refreshAuthStatus().then(() => {
if (isAuthenticated) {
loadProfiles();
}
});
});
return unsubscribe;
}, [navigation, refreshAuthStatus, isAuthenticated, loadProfiles]);
// Save profiles to AsyncStorage
const saveProfiles = useCallback(async (updatedProfiles: Profile[]) => {
try {
await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles));
} catch (error) {
if (__DEV__) console.error('Error saving profiles:', error);
openAlert('Error', 'Failed to save profiles');
}
}, []);
useEffect(() => {
// Only authenticated users can access profiles
if (!isAuthenticated) {
navigation.goBack();
return;
}
loadProfiles();
}, [isAuthenticated, loadProfiles, navigation]);
const handleAddProfile = useCallback(() => {
if (!newProfileName.trim()) {
openAlert('Error', 'Please enter a profile name');
return;
}
const newProfile: Profile = {
id: new Date().getTime().toString(),
name: newProfileName.trim(),
isActive: false,
createdAt: new Date().getTime()
};
const updatedProfiles = [...profiles, newProfile];
setProfiles(updatedProfiles);
saveProfiles(updatedProfiles);
setNewProfileName('');
setShowAddModal(false);
}, [newProfileName, profiles, saveProfiles]);
const handleSelectProfile = useCallback((id: string) => {
const updatedProfiles = profiles.map(profile => ({
...profile,
isActive: profile.id === id
}));
setProfiles(updatedProfiles);
saveProfiles(updatedProfiles);
}, [profiles, saveProfiles]);
const handleDeleteProfile = useCallback((id: string) => {
// Prevent deleting the active profile
const isActiveProfile = profiles.find(p => p.id === id)?.isActive;
if (isActiveProfile) {
openAlert('Error', 'Cannot delete the active profile. Switch to another profile first.');
return;
}
// Prevent deleting the last profile
if (profiles.length <= 1) {
openAlert('Error', 'Cannot delete the only profile');
return;
}
openAlert(
'Delete Profile',
'Are you sure you want to delete this profile? This action cannot be undone.',
[
{ label: 'Cancel', onPress: () => { } },
{
label: 'Delete',
onPress: () => {
const updatedProfiles = profiles.filter(profile => profile.id !== id);
setProfiles(updatedProfiles);
saveProfiles(updatedProfiles);
}
}
]
);
}, [profiles, saveProfiles]);
const handleBack = () => {
navigation.goBack();
};
const renderItem = ({ item }: { item: Profile }) => (
<View style={styles.profileItem}>
<FocusableTouchableOpacity
style={[
styles.profileContent,
item.isActive && {
backgroundColor: `${currentTheme.colors.primary}30`,
borderColor: currentTheme.colors.primary
}
]}
onPress={() => handleSelectProfile(item.id)}
>
<View style={styles.avatarContainer}>
<MaterialIcons
name="account-circle"
size={40}
color={item.isActive ? currentTheme.colors.primary : currentTheme.colors.text}
/>
</View>
<View style={styles.profileInfo}>
<Text style={[styles.profileName, { color: currentTheme.colors.text }]}>
{item.name}
</Text>
{item.isActive && (
<Text style={[styles.activeLabel, { color: currentTheme.colors.primary }]}>
Active
</Text>
)}
</View>
{!item.isActive && (
<FocusableTouchableOpacity
style={styles.deleteButton}
onPress={() => handleDeleteProfile(item.id)}
>
<MaterialIcons name="delete" size={24} color={currentTheme.colors.error} />
</FocusableTouchableOpacity>
)}
</FocusableTouchableOpacity>
</View>
);
return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
<View style={styles.header}>
<FocusableTouchableOpacity
onPress={handleBack}
style={styles.backButton}
activeOpacity={0.7}
>
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
/>
</FocusableTouchableOpacity>
<Text
style={[
styles.headerTitle,
{ color: currentTheme.colors.text },
]}
>
Profiles
</Text>
</View>
<View style={styles.content}>
<FlatList
data={profiles}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.listContent}
ListHeaderComponent={
<Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted }]}>
MANAGE PROFILES
</Text>
}
ListFooterComponent={
<FocusableTouchableOpacity
style={[
styles.addButton,
{ backgroundColor: currentTheme.colors.elevation2 }
]}
onPress={() => setShowAddModal(true)}
>
<MaterialIcons name="add" size={24} color={currentTheme.colors.primary} />
<Text style={[styles.addButtonText, { color: currentTheme.colors.text }]}>
Add New Profile
</Text>
</FocusableTouchableOpacity>
}
/>
</View>
{/* Modal for adding a new profile */}
<Modal
visible={showAddModal}
transparent
animationType="fade"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setShowAddModal(false)}
>
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: currentTheme.colors.elevation2 }]}>
<Text style={[styles.modalTitle, { color: currentTheme.colors.text }]}>
Create New Profile
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: `${currentTheme.colors.textMuted}20`,
color: currentTheme.colors.text,
borderColor: currentTheme.colors.border
}
]}
placeholder="Profile Name"
placeholderTextColor={currentTheme.colors.textMuted}
value={newProfileName}
onChangeText={setNewProfileName}
autoFocus
/>
<View style={styles.modalButtons}>
<FocusableTouchableOpacity
style={[styles.modalButton, styles.cancelButton]}
onPress={() => {
setNewProfileName('');
setShowAddModal(false);
}}
>
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
</FocusableTouchableOpacity>
<FocusableTouchableOpacity
style={[
styles.modalButton,
styles.createButton,
{ backgroundColor: currentTheme.colors.primary }
]}
onPress={handleAddProfile}
>
<Text style={{ color: '#fff' }}>Create</Text>
</FocusableTouchableOpacity>
</View>
</View>
</View>
</Modal>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
paddingBottom: 8,
},
backButton: {
padding: 8,
marginRight: 16,
borderRadius: 20,
},
headerTitle: {
fontSize: 20,
fontWeight: '600',
},
content: {
flex: 1,
paddingHorizontal: 16,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
marginTop: 24,
marginBottom: 12,
letterSpacing: 0.5,
},
listContent: {
paddingBottom: 24,
},
profileItem: {
marginBottom: 12,
},
profileContent: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderRadius: 12,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
avatarContainer: {
marginRight: 16,
},
profileInfo: {
flex: 1,
},
profileName: {
fontSize: 16,
fontWeight: '500',
},
activeLabel: {
fontSize: 12,
marginTop: 4,
fontWeight: '500',
},
deleteButton: {
padding: 8,
},
addButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 16,
borderRadius: 12,
marginTop: 12,
},
addButtonText: {
fontSize: 16,
fontWeight: '500',
marginLeft: 8,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
modalContent: {
width: '100%',
borderRadius: 16,
padding: 24,
},
modalTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 24,
textAlign: 'center',
},
input: {
width: '100%',
height: 50,
borderRadius: 8,
paddingHorizontal: 16,
marginBottom: 24,
borderWidth: 1,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
},
modalButton: {
flex: 1,
height: 44,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
cancelButton: {
marginRight: 8,
},
createButton: {
marginLeft: 8,
},
});
export default ProfilesScreen;