Enhance Trakt integration by adding refreshAuthStatus function and ProfilesSettings screen

This update introduces a new `refreshAuthStatus` function in the TraktContext and hooks, allowing for manual refresh of authentication status. Additionally, a new `ProfilesSettings` screen has been added to the navigation stack, enabling users to manage profiles. The SettingsScreen has been updated to trigger a refresh of the auth status when focused, improving user experience. Styling adjustments have been made to accommodate the new profiles feature.
This commit is contained in:
tapframe 2025-05-04 03:01:21 +05:30
parent 953556c65a
commit 64193b4154
6 changed files with 634 additions and 4 deletions

View file

@ -9,6 +9,7 @@ interface TraktContextProps {
watchedMovies: TraktWatchedItem[];
watchedShows: TraktWatchedItem[];
checkAuthStatus: () => Promise<void>;
refreshAuthStatus: () => Promise<void>;
loadWatchedItems: () => Promise<void>;
isMovieWatched: (imdbId: string) => Promise<boolean>;
isEpisodeWatched: (imdbId: string, season: number, episode: number) => Promise<boolean>;

View file

@ -8,6 +8,7 @@ export function useTraktIntegration() {
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
const [watchedMovies, setWatchedMovies] = useState<TraktWatchedItem[]>([]);
const [watchedShows, setWatchedShows] = useState<TraktWatchedItem[]>([]);
const [lastAuthCheck, setLastAuthCheck] = useState<number>(Date.now());
// Check authentication status
const checkAuthStatus = useCallback(async () => {
@ -22,6 +23,9 @@ export function useTraktIntegration() {
} else {
setUserProfile(null);
}
// Update the last auth check timestamp to trigger dependent components to update
setLastAuthCheck(Date.now());
} catch (error) {
logger.error('[useTraktIntegration] Error checking auth status:', error);
} finally {
@ -29,6 +33,12 @@ export function useTraktIntegration() {
}
}, []);
// Function to force refresh the auth status
const refreshAuthStatus = useCallback(async () => {
logger.log('[useTraktIntegration] Refreshing auth status');
await checkAuthStatus();
}, [checkAuthStatus]);
// Load watched items
const loadWatchedItems = useCallback(async () => {
if (!isAuthenticated) return;
@ -141,6 +151,7 @@ export function useTraktIntegration() {
isMovieWatched,
isEpisodeWatched,
markMovieAsWatched,
markEpisodeAsWatched
markEpisodeAsWatched,
refreshAuthStatus
};
}

View file

@ -38,6 +38,7 @@ import TraktSettingsScreen from '../screens/TraktSettingsScreen';
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
import LogoSourceSettings from '../screens/LogoSourceSettings';
import ThemeScreen from '../screens/ThemeScreen';
import ProfilesScreen from '../screens/ProfilesScreen';
// Stack navigator types
export type RootStackParamList = {
@ -95,6 +96,7 @@ export type RootStackParamList = {
PlayerSettings: undefined;
LogoSourceSettings: undefined;
ThemeSettings: undefined;
ProfilesSettings: undefined;
};
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
@ -858,6 +860,21 @@ const AppNavigator = () => {
},
}}
/>
<Stack.Screen
name="ProfilesSettings"
component={ProfilesScreen}
options={{
animation: 'fade',
animationDuration: 200,
presentation: 'card',
gestureEnabled: true,
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
</Stack.Navigator>
</PaperProvider>
</SafeAreaProvider>

View file

@ -0,0 +1,441 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
FlatList,
Alert,
StatusBar,
Platform,
SafeAreaView,
TextInput,
Modal
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '../contexts/ThemeContext';
import { useTraktContext } from '../contexts/TraktContext';
import AsyncStorage from '@react-native-async-storage/async-storage';
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);
// Load profiles from AsyncStorage
const loadProfiles = useCallback(async () => {
try {
setIsLoading(true);
const storedProfiles = await AsyncStorage.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 AsyncStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify([defaultProfile]));
}
} catch (error) {
console.error('Error loading profiles:', error);
Alert.alert('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 AsyncStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles));
} catch (error) {
console.error('Error saving profiles:', error);
Alert.alert('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()) {
Alert.alert('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) {
Alert.alert('Error', 'Cannot delete the active profile. Switch to another profile first.');
return;
}
// Prevent deleting the last profile
if (profiles.length <= 1) {
Alert.alert('Error', 'Cannot delete the only profile');
return;
}
Alert.alert(
'Delete Profile',
'Are you sure you want to delete this profile? This action cannot be undone.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
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}>
<TouchableOpacity
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 && (
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleDeleteProfile(item.id)}
>
<MaterialIcons name="delete" size={24} color={currentTheme.colors.error} />
</TouchableOpacity>
)}
</TouchableOpacity>
</View>
);
return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
<View style={styles.header}>
<TouchableOpacity
onPress={handleBack}
style={styles.backButton}
activeOpacity={0.7}
>
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
/>
</TouchableOpacity>
<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={
<TouchableOpacity
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>
</TouchableOpacity>
}
/>
</View>
{/* Modal for adding a new profile */}
<Modal
visible={showAddModal}
transparent
animationType="fade"
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}>
<TouchableOpacity
style={[styles.modalButton, styles.cancelButton]}
onPress={() => {
setNewProfileName('');
setShowAddModal(false);
}}
>
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.modalButton,
styles.createButton,
{ backgroundColor: currentTheme.colors.primary }
]}
onPress={handleAddProfile}
>
<Text style={{ color: '#fff' }}>Create</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</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;

View file

@ -125,10 +125,27 @@ const SettingsScreen: React.FC = () => {
const { settings, updateSetting } = useSettings();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { lastUpdate } = useCatalogContext();
const { isAuthenticated, userProfile } = useTraktContext();
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
// Add a useEffect to check authentication status on focus
useEffect(() => {
// This will reload the Trakt auth status whenever the settings screen is focused
const unsubscribe = navigation.addListener('focus', () => {
// Force a re-render when returning to this screen
// This will reflect the updated isAuthenticated state from the TraktContext
// Refresh auth status
if (isAuthenticated || userProfile) {
// Just to be cautious, log the current state
console.log('SettingsScreen focused, refreshing auth status. Current state:', { isAuthenticated, userProfile: userProfile?.username });
}
refreshAuthStatus();
});
return unsubscribe;
}, [navigation, isAuthenticated, userProfile, refreshAuthStatus]);
// States for dynamic content
const [addonCount, setAddonCount] = useState<number>(0);
const [catalogCount, setCatalogCount] = useState<number>(0);
@ -268,6 +285,81 @@ const SettingsScreen: React.FC = () => {
/>
</SettingsCard>
<SettingsCard title="Profiles">
{isAuthenticated ? (
<SettingItem
title="Manage Profiles"
description="Create and switch between profiles"
icon="people"
renderControl={ChevronRight}
onPress={() => navigation.navigate('ProfilesSettings')}
isLast={true}
/>
) : (
<TouchableOpacity
style={[
styles.profileLockContainer,
{
backgroundColor: `${currentTheme.colors.primary}10`,
borderWidth: 1,
borderColor: `${currentTheme.colors.primary}30`
}
]}
activeOpacity={1}
>
<View style={styles.profileLockContent}>
<MaterialIcons name="lock-outline" size={24} color={currentTheme.colors.primary} />
<View style={styles.profileLockTextContainer}>
<Text style={[styles.profileLockTitle, { color: currentTheme.colors.text }]}>
Sign in to use Profiles
</Text>
<Text style={[styles.profileLockDescription, { color: currentTheme.colors.textMuted }]}>
Create multiple profiles for different users and preferences
</Text>
</View>
</View>
<View style={styles.profileBenefits}>
<View style={styles.benefitCol}>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.benefitText, { color: currentTheme.colors.textMuted }]}>
Separate watchlists
</Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.benefitText, { color: currentTheme.colors.textMuted }]}>
Content preferences
</Text>
</View>
</View>
<View style={styles.benefitCol}>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.benefitText, { color: currentTheme.colors.textMuted }]}>
Personalized recommendations
</Text>
</View>
<View style={styles.benefitItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.benefitText, { color: currentTheme.colors.textMuted }]}>
Individual viewing history
</Text>
</View>
</View>
</View>
<TouchableOpacity
style={[styles.loginButton, { backgroundColor: currentTheme.colors.primary }]}
activeOpacity={0.7}
onPress={() => navigation.navigate('TraktSettings')}
>
<Text style={styles.loginButtonText}>Connect with Trakt</Text>
<MaterialIcons name="arrow-forward" size={18} color="#FFFFFF" style={styles.loginButtonIcon} />
</TouchableOpacity>
</TouchableOpacity>
)}
</SettingsCard>
<SettingsCard title="Appearance">
<SettingItem
title="Theme"
@ -567,6 +659,62 @@ const styles = StyleSheet.create({
fontSize: 14,
fontWeight: '500',
},
profileLockContainer: {
padding: 16,
borderRadius: 8,
overflow: 'hidden',
marginVertical: 8,
},
profileLockContent: {
flexDirection: 'row',
alignItems: 'center',
},
profileLockTextContainer: {
flex: 1,
marginHorizontal: 12,
},
profileLockTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
profileLockDescription: {
fontSize: 14,
opacity: 0.8,
},
profileBenefits: {
flexDirection: 'row',
marginTop: 16,
justifyContent: 'space-between',
},
benefitCol: {
flex: 1,
},
benefitItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
benefitText: {
fontSize: 14,
marginLeft: 8,
},
loginButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 8,
paddingVertical: 12,
marginTop: 16,
},
loginButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
loginButtonIcon: {
marginLeft: 8,
},
});
export default SettingsScreen;

View file

@ -94,7 +94,19 @@ const TraktSettingsScreen: React.FC = () => {
.then(success => {
if (success) {
logger.log('[TraktSettingsScreen] Token exchange successful');
checkAuthStatus();
checkAuthStatus().then(() => {
// Show success message
Alert.alert(
'Successfully Connected',
'Your Trakt account has been connected successfully.',
[
{
text: 'OK',
onPress: () => navigation.goBack()
}
]
);
});
} else {
logger.error('[TraktSettingsScreen] Token exchange failed');
Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.');
@ -116,7 +128,7 @@ const TraktSettingsScreen: React.FC = () => {
setIsExchangingCode(false);
}
}
}, [response, checkAuthStatus, request?.codeVerifier]);
}, [response, checkAuthStatus, request?.codeVerifier, navigation]);
const handleSignIn = () => {
promptAsync(); // Trigger the authentication flow