diff --git a/app.json b/app.json index a5b08661..b611bcb0 100644 --- a/app.json +++ b/app.json @@ -6,6 +6,7 @@ "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "light", + "scheme": "stremioexpo", "newArchEnabled": true, "splash": { "image": "./assets/splash-icon.png", diff --git a/assets/WhatsApp Image 2025-04-24 at 09.47.46_1d72ceb0.jpg b/assets/WhatsApp Image 2025-04-24 at 09.47.46_1d72ceb0.jpg new file mode 100644 index 00000000..69457cce Binary files /dev/null and b/assets/WhatsApp Image 2025-04-24 at 09.47.46_1d72ceb0.jpg differ diff --git a/src/assets/Desktop (1).png b/src/assets/Desktop (1).png new file mode 100644 index 00000000..f99fcab3 Binary files /dev/null and b/src/assets/Desktop (1).png differ diff --git a/src/assets/app-icon.png b/src/assets/app-icon.png deleted file mode 100644 index 0692e313..00000000 Binary files a/src/assets/app-icon.png and /dev/null differ diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 43ec13cf..683de09a 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -28,11 +28,16 @@ interface ContinueWatchingItem extends StreamingContent { episodeTitle?: string; } +// Define the ref interface +interface ContinueWatchingRef { + refresh: () => Promise; +} + const { width } = Dimensions.get('window'); const POSTER_WIDTH = (width - 40) / 2.7; -// Create a proper imperative handle with React.forwardRef -const ContinueWatchingSection = React.forwardRef<{ refresh: () => Promise }>((props, ref) => { +// Create a proper imperative handle with React.forwardRef and updated type +const ContinueWatchingSection = React.forwardRef((props, ref) => { const navigation = useNavigation>(); const [continueWatchingItems, setContinueWatchingItems] = useState([]); const [loading, setLoading] = useState(true); @@ -188,7 +193,11 @@ const ContinueWatchingSection = React.forwardRef<{ refresh: () => Promise // Properly expose the refresh method React.useImperativeHandle(ref, () => ({ - refresh: loadContinueWatching + refresh: async () => { + await loadContinueWatching(); + // Return whether there are items to help parent determine visibility + return continueWatchingItems.length > 0; + } })); const handleContentPress = useCallback((id: string, type: string) => { @@ -362,6 +371,15 @@ const styles = StyleSheet.create({ height: '100%', backgroundColor: colors.primary, }, + emptyContainer: { + paddingHorizontal: 16, + justifyContent: 'center', + alignItems: 'center', + }, + emptyText: { + color: colors.textMuted, + fontSize: 14, + }, }); export default React.memo(ContinueWatchingSection); \ No newline at end of file diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index aef55096..b06a7855 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -18,7 +18,7 @@ import { Modal, Pressable } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; import { StreamingContent, CatalogContent, catalogService } from '../services/catalogService'; @@ -74,6 +74,10 @@ interface DropUpMenuProps { onOptionSelect: (option: string) => void; } +interface ContinueWatchingRef { + refresh: () => Promise; +} + const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { const translateY = useSharedValue(300); const opacity = useSharedValue(0); @@ -354,11 +358,12 @@ const SkeletonFeatured = () => ( const HomeScreen = () => { const navigation = useNavigation>(); const isDarkMode = useColorScheme() === 'dark'; - const continueWatchingRef = useRef<{ refresh: () => Promise }>(null); + const continueWatchingRef = useRef(null); const { settings } = useSettings(); const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection); const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource); const refreshTimeoutRef = useRef(null); + const [hasContinueWatching, setHasContinueWatching] = useState(false); const { catalogs, @@ -408,9 +413,25 @@ const HomeScreen = () => { }; }, [featuredContentSource, showHeroSection, refreshFeatured]); + useFocusEffect( + useCallback(() => { + const statusBarConfig = () => { + StatusBar.setBarStyle("light-content"); + StatusBar.setTranslucent(true); + StatusBar.setBackgroundColor('transparent'); + }; + + statusBarConfig(); + + return () => { + // Don't change StatusBar settings when unfocusing to prevent layout shifts + // Only set these when component unmounts completely + }; + }, []) + ); + useEffect(() => { - StatusBar.setTranslucent(true); - StatusBar.setBackgroundColor('transparent'); + // Only run cleanup when component unmounts completely, not on unfocus return () => { StatusBar.setTranslucent(false); StatusBar.setBackgroundColor(colors.darkBackground); @@ -484,9 +505,10 @@ const HomeScreen = () => { }); }, [featuredContent, navigation]); - const refreshContinueWatching = useCallback(() => { + const refreshContinueWatching = useCallback(async () => { if (continueWatchingRef.current) { - continueWatchingRef.current.refresh(); + const hasContent = await continueWatchingRef.current.refresh(); + setHasContinueWatching(hasContent); } }, []); @@ -695,7 +717,7 @@ const HomeScreen = () => { } return ( - + { colors={[colors.primary, colors.secondary]} /> } - contentContainerStyle={styles.scrollContent} + contentContainerStyle={[ + styles.scrollContent, + { paddingTop: Platform.OS === 'ios' ? 0 : 0 } + ]} showsVerticalScrollIndicator={false} > {showHeroSection && renderFeaturedContent()} @@ -719,9 +744,11 @@ const HomeScreen = () => { - - - + {hasContinueWatching && ( + + + + )} {catalogs.length > 0 ? ( catalogs.map((catalog, index) => ( @@ -747,7 +774,7 @@ const HomeScreen = () => { ) )} - + ); }; @@ -770,7 +797,7 @@ const styles = StyleSheet.create({ featuredContainer: { width: '100%', height: height * 0.6, - marginTop: Platform.OS === 'ios' ? 85 : 75, + marginTop: Platform.OS === 'ios' ? 0 : 0, marginBottom: 8, position: 'relative', }, diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index cf89f567..81214448 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -11,11 +11,9 @@ import { ScrollView, StatusBar, Platform, - Linking } from 'react-native'; import { useNavigation } from '@react-navigation/native'; -import * as WebBrowser from 'expo-web-browser'; -import { makeRedirectUri } from 'expo-auth-session'; +import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import { traktService, TraktUser } from '../services/traktService'; import { colors } from '../styles/colors'; @@ -25,6 +23,14 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg'; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; +// Trakt configuration (replace with your actual Client ID if different) +const TRAKT_CLIENT_ID = 'd7271f7dd57d8aeff63e99408610091a6b1ceac3b3a541d1031a48f429b7942c'; +const discovery = { + authorizationEndpoint: 'https://trakt.tv/oauth/authorize', + // Note: Trakt doesn't use a standard token endpoint for the auth code flow + // We'll handle the code exchange manually in `traktService` +}; + // For use with deep linking const redirectUri = makeRedirectUri({ scheme: 'stremioexpo', @@ -36,7 +42,6 @@ const TraktSettingsScreen: React.FC = () => { const isDarkMode = settings.enableDarkMode; const navigation = useNavigation(); const [isLoading, setIsLoading] = useState(true); - const [isAuthenticating, setIsAuthenticating] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false); const [userProfile, setUserProfile] = useState(null); @@ -49,6 +54,8 @@ const TraktSettingsScreen: React.FC = () => { if (authenticated) { const profile = await traktService.getUserProfile(); setUserProfile(profile); + } else { + setUserProfile(null); // Ensure profile is cleared if not authenticated } } catch (error) { logger.error('[TraktSettingsScreen] Error checking auth status:', error); @@ -61,45 +68,57 @@ const TraktSettingsScreen: React.FC = () => { checkAuthStatus(); }, [checkAuthStatus]); - // Handle deep linking when returning from Trakt authorization + // Setup expo-auth-session hook + const [request, response, promptAsync] = useAuthRequest( + { + clientId: TRAKT_CLIENT_ID, + scopes: [], // Trakt doesn't use scopes for standard auth code flow + redirectUri: redirectUri, + responseType: ResponseType.Code, // Ask for the authorization code + }, + discovery + ); + + const [isExchangingCode, setIsExchangingCode] = useState(false); + + // Handle the response from the auth request useEffect(() => { - const handleRedirect = async (event: { url: string }) => { - const { url } = event; - if (url.includes('auth/trakt')) { - setIsAuthenticating(true); - try { - const code = url.split('code=')[1].split('&')[0]; - const success = await traktService.exchangeCodeForToken(code); - if (success) { - checkAuthStatus(); - } else { - Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.'); - } - } catch (error) { - logger.error('[TraktSettingsScreen] Authentication error:', error); - Alert.alert('Authentication Error', 'An error occurred during authentication.'); - } finally { - setIsAuthenticating(false); - } + if (response) { + setIsExchangingCode(true); // Indicate we're processing the response + if (response.type === 'success') { + const { code } = response.params; + logger.log('[TraktSettingsScreen] Auth code received:', code); + traktService.exchangeCodeForToken(code) + .then(success => { + if (success) { + logger.log('[TraktSettingsScreen] Token exchange successful'); + checkAuthStatus(); // Re-check auth status and fetch profile + } else { + logger.error('[TraktSettingsScreen] Token exchange failed'); + Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.'); + } + }) + .catch(error => { + logger.error('[TraktSettingsScreen] Token exchange error:', error); + Alert.alert('Authentication Error', 'An error occurred during authentication.'); + }) + .finally(() => { + setIsExchangingCode(false); + }); + } else if (response.type === 'error') { + logger.error('[TraktSettingsScreen] Authentication error:', response.error); + Alert.alert('Authentication Error', response.error?.message || 'An error occurred during authentication.'); + setIsExchangingCode(false); + } else { + // Handle other response types like 'cancel', 'dismiss' if needed + logger.log('[TraktSettingsScreen] Auth response type:', response.type); + setIsExchangingCode(false); } - }; - - // Add event listener for deep linking - const subscription = Linking.addEventListener('url', handleRedirect); - - return () => { - subscription.remove(); - }; - }, [checkAuthStatus]); - - const handleSignIn = async () => { - try { - const authUrl = traktService.getAuthUrl(); - await WebBrowser.openAuthSessionAsync(authUrl, redirectUri); - } catch (error) { - logger.error('[TraktSettingsScreen] Error opening auth session:', error); - Alert.alert('Authentication Error', 'Could not open Trakt authentication page.'); } + }, [response, checkAuthStatus]); + + const handleSignIn = () => { + promptAsync(); // Trigger the authentication flow }; const handleSignOut = async () => { @@ -249,9 +268,9 @@ const TraktSettingsScreen: React.FC = () => { { backgroundColor: isDarkMode ? colors.primary : colors.primary } ]} onPress={handleSignIn} - disabled={isAuthenticating} + disabled={!request || isExchangingCode} // Disable while waiting for response or exchanging code > - {isAuthenticating ? ( + {isExchangingCode ? ( ) : (