From 64193b41549b2dfefea1e895ece754f70619f65d Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 4 May 2025 03:01:21 +0530 Subject: [PATCH] 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. --- src/contexts/TraktContext.tsx | 1 + src/hooks/useTraktIntegration.ts | 13 +- src/navigation/AppNavigator.tsx | 17 ++ src/screens/ProfilesScreen.tsx | 441 ++++++++++++++++++++++++++++ src/screens/SettingsScreen.tsx | 150 +++++++++- src/screens/TraktSettingsScreen.tsx | 16 +- 6 files changed, 634 insertions(+), 4 deletions(-) create mode 100644 src/screens/ProfilesScreen.tsx diff --git a/src/contexts/TraktContext.tsx b/src/contexts/TraktContext.tsx index b7949cb1..05c27d13 100644 --- a/src/contexts/TraktContext.tsx +++ b/src/contexts/TraktContext.tsx @@ -9,6 +9,7 @@ interface TraktContextProps { watchedMovies: TraktWatchedItem[]; watchedShows: TraktWatchedItem[]; checkAuthStatus: () => Promise; + refreshAuthStatus: () => Promise; loadWatchedItems: () => Promise; isMovieWatched: (imdbId: string) => Promise; isEpisodeWatched: (imdbId: string, season: number, episode: number) => Promise; diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts index d89ac19a..692cdaa8 100644 --- a/src/hooks/useTraktIntegration.ts +++ b/src/hooks/useTraktIntegration.ts @@ -8,6 +8,7 @@ export function useTraktIntegration() { const [userProfile, setUserProfile] = useState(null); const [watchedMovies, setWatchedMovies] = useState([]); const [watchedShows, setWatchedShows] = useState([]); + const [lastAuthCheck, setLastAuthCheck] = useState(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 }; } \ No newline at end of file diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 2ee6a77b..6720cda3 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -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; @@ -858,6 +860,21 @@ const AppNavigator = () => { }, }} /> + diff --git a/src/screens/ProfilesScreen.tsx b/src/screens/ProfilesScreen.tsx new file mode 100644 index 00000000..18c6130c --- /dev/null +++ b/src/screens/ProfilesScreen.tsx @@ -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([]); + 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 }) => ( + + handleSelectProfile(item.id)} + > + + + + + + {item.name} + + {item.isActive && ( + + Active + + )} + + {!item.isActive && ( + handleDeleteProfile(item.id)} + > + + + )} + + + ); + + return ( + + + + + + + + + Profiles + + + + + item.id} + contentContainerStyle={styles.listContent} + ListHeaderComponent={ + + MANAGE PROFILES + + } + ListFooterComponent={ + setShowAddModal(true)} + > + + + Add New Profile + + + } + /> + + + {/* Modal for adding a new profile */} + setShowAddModal(false)} + > + + + + Create New Profile + + + + + + { + setNewProfileName(''); + setShowAddModal(false); + }} + > + Cancel + + + Create + + + + + + + ); +}; + +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; \ No newline at end of file diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 23b0c4d6..a0cd3b7c 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -125,10 +125,27 @@ const SettingsScreen: React.FC = () => { const { settings, updateSetting } = useSettings(); const navigation = useNavigation>(); 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(0); const [catalogCount, setCatalogCount] = useState(0); @@ -268,6 +285,81 @@ const SettingsScreen: React.FC = () => { /> + + {isAuthenticated ? ( + navigation.navigate('ProfilesSettings')} + isLast={true} + /> + ) : ( + + + + + + Sign in to use Profiles + + + Create multiple profiles for different users and preferences + + + + + + + + + Separate watchlists + + + + + + Content preferences + + + + + + + + Personalized recommendations + + + + + + Individual viewing history + + + + + navigation.navigate('TraktSettings')} + > + Connect with Trakt + + + + )} + + { .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