mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-25 18:42:53 +00:00
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:
parent
953556c65a
commit
64193b4154
6 changed files with 634 additions and 4 deletions
|
|
@ -9,6 +9,7 @@ interface TraktContextProps {
|
||||||
watchedMovies: TraktWatchedItem[];
|
watchedMovies: TraktWatchedItem[];
|
||||||
watchedShows: TraktWatchedItem[];
|
watchedShows: TraktWatchedItem[];
|
||||||
checkAuthStatus: () => Promise<void>;
|
checkAuthStatus: () => Promise<void>;
|
||||||
|
refreshAuthStatus: () => Promise<void>;
|
||||||
loadWatchedItems: () => Promise<void>;
|
loadWatchedItems: () => Promise<void>;
|
||||||
isMovieWatched: (imdbId: string) => Promise<boolean>;
|
isMovieWatched: (imdbId: string) => Promise<boolean>;
|
||||||
isEpisodeWatched: (imdbId: string, season: number, episode: number) => Promise<boolean>;
|
isEpisodeWatched: (imdbId: string, season: number, episode: number) => Promise<boolean>;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export function useTraktIntegration() {
|
||||||
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
|
||||||
const [watchedMovies, setWatchedMovies] = useState<TraktWatchedItem[]>([]);
|
const [watchedMovies, setWatchedMovies] = useState<TraktWatchedItem[]>([]);
|
||||||
const [watchedShows, setWatchedShows] = useState<TraktWatchedItem[]>([]);
|
const [watchedShows, setWatchedShows] = useState<TraktWatchedItem[]>([]);
|
||||||
|
const [lastAuthCheck, setLastAuthCheck] = useState<number>(Date.now());
|
||||||
|
|
||||||
// Check authentication status
|
// Check authentication status
|
||||||
const checkAuthStatus = useCallback(async () => {
|
const checkAuthStatus = useCallback(async () => {
|
||||||
|
|
@ -22,6 +23,9 @@ export function useTraktIntegration() {
|
||||||
} else {
|
} else {
|
||||||
setUserProfile(null);
|
setUserProfile(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the last auth check timestamp to trigger dependent components to update
|
||||||
|
setLastAuthCheck(Date.now());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[useTraktIntegration] Error checking auth status:', error);
|
logger.error('[useTraktIntegration] Error checking auth status:', error);
|
||||||
} finally {
|
} 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
|
// Load watched items
|
||||||
const loadWatchedItems = useCallback(async () => {
|
const loadWatchedItems = useCallback(async () => {
|
||||||
if (!isAuthenticated) return;
|
if (!isAuthenticated) return;
|
||||||
|
|
@ -141,6 +151,7 @@ export function useTraktIntegration() {
|
||||||
isMovieWatched,
|
isMovieWatched,
|
||||||
isEpisodeWatched,
|
isEpisodeWatched,
|
||||||
markMovieAsWatched,
|
markMovieAsWatched,
|
||||||
markEpisodeAsWatched
|
markEpisodeAsWatched,
|
||||||
|
refreshAuthStatus
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ import TraktSettingsScreen from '../screens/TraktSettingsScreen';
|
||||||
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
||||||
import LogoSourceSettings from '../screens/LogoSourceSettings';
|
import LogoSourceSettings from '../screens/LogoSourceSettings';
|
||||||
import ThemeScreen from '../screens/ThemeScreen';
|
import ThemeScreen from '../screens/ThemeScreen';
|
||||||
|
import ProfilesScreen from '../screens/ProfilesScreen';
|
||||||
|
|
||||||
// Stack navigator types
|
// Stack navigator types
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
|
|
@ -95,6 +96,7 @@ export type RootStackParamList = {
|
||||||
PlayerSettings: undefined;
|
PlayerSettings: undefined;
|
||||||
LogoSourceSettings: undefined;
|
LogoSourceSettings: undefined;
|
||||||
ThemeSettings: undefined;
|
ThemeSettings: undefined;
|
||||||
|
ProfilesSettings: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
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>
|
</Stack.Navigator>
|
||||||
</PaperProvider>
|
</PaperProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
|
|
|
||||||
441
src/screens/ProfilesScreen.tsx
Normal file
441
src/screens/ProfilesScreen.tsx
Normal 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;
|
||||||
|
|
@ -125,10 +125,27 @@ const SettingsScreen: React.FC = () => {
|
||||||
const { settings, updateSetting } = useSettings();
|
const { settings, updateSetting } = useSettings();
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { lastUpdate } = useCatalogContext();
|
const { lastUpdate } = useCatalogContext();
|
||||||
const { isAuthenticated, userProfile } = useTraktContext();
|
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
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
|
// States for dynamic content
|
||||||
const [addonCount, setAddonCount] = useState<number>(0);
|
const [addonCount, setAddonCount] = useState<number>(0);
|
||||||
const [catalogCount, setCatalogCount] = useState<number>(0);
|
const [catalogCount, setCatalogCount] = useState<number>(0);
|
||||||
|
|
@ -268,6 +285,81 @@ const SettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</SettingsCard>
|
</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">
|
<SettingsCard title="Appearance">
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="Theme"
|
title="Theme"
|
||||||
|
|
@ -567,6 +659,62 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
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;
|
export default SettingsScreen;
|
||||||
|
|
@ -94,7 +94,19 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
.then(success => {
|
.then(success => {
|
||||||
if (success) {
|
if (success) {
|
||||||
logger.log('[TraktSettingsScreen] Token exchange successful');
|
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 {
|
} else {
|
||||||
logger.error('[TraktSettingsScreen] Token exchange failed');
|
logger.error('[TraktSettingsScreen] Token exchange failed');
|
||||||
Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.');
|
Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.');
|
||||||
|
|
@ -116,7 +128,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
setIsExchangingCode(false);
|
setIsExchangingCode(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [response, checkAuthStatus, request?.codeVerifier]);
|
}, [response, checkAuthStatus, request?.codeVerifier, navigation]);
|
||||||
|
|
||||||
const handleSignIn = () => {
|
const handleSignIn = () => {
|
||||||
promptAsync(); // Trigger the authentication flow
|
promptAsync(); // Trigger the authentication flow
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue