Update app configuration and enhance ContinueWatchingSection; add scheme to app.json, remove unused app icon, and improve refresh logic in ContinueWatchingSection for better state management. Update HomeScreen to conditionally render ContinueWatchingSection based on content availability.

This commit is contained in:
tapframe 2025-04-26 12:09:05 +05:30
parent c95d9d8093
commit b1e1017288
7 changed files with 123 additions and 58 deletions

View file

@ -6,6 +6,7 @@
"orientation": "default", "orientation": "default",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"userInterfaceStyle": "light", "userInterfaceStyle": "light",
"scheme": "stremioexpo",
"newArchEnabled": true, "newArchEnabled": true,
"splash": { "splash": {
"image": "./assets/splash-icon.png", "image": "./assets/splash-icon.png",

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
src/assets/Desktop (1).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -28,11 +28,16 @@ interface ContinueWatchingItem extends StreamingContent {
episodeTitle?: string; episodeTitle?: string;
} }
// Define the ref interface
interface ContinueWatchingRef {
refresh: () => Promise<boolean>;
}
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const POSTER_WIDTH = (width - 40) / 2.7; const POSTER_WIDTH = (width - 40) / 2.7;
// Create a proper imperative handle with React.forwardRef // Create a proper imperative handle with React.forwardRef and updated type
const ContinueWatchingSection = React.forwardRef<{ refresh: () => Promise<void> }>((props, ref) => { const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const [continueWatchingItems, setContinueWatchingItems] = useState<ContinueWatchingItem[]>([]); const [continueWatchingItems, setContinueWatchingItems] = useState<ContinueWatchingItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -188,7 +193,11 @@ const ContinueWatchingSection = React.forwardRef<{ refresh: () => Promise<void>
// Properly expose the refresh method // Properly expose the refresh method
React.useImperativeHandle(ref, () => ({ 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) => { const handleContentPress = useCallback((id: string, type: string) => {
@ -362,6 +371,15 @@ const styles = StyleSheet.create({
height: '100%', height: '100%',
backgroundColor: colors.primary, backgroundColor: colors.primary,
}, },
emptyContainer: {
paddingHorizontal: 16,
justifyContent: 'center',
alignItems: 'center',
},
emptyText: {
color: colors.textMuted,
fontSize: 14,
},
}); });
export default React.memo(ContinueWatchingSection); export default React.memo(ContinueWatchingSection);

View file

@ -18,7 +18,7 @@ import {
Modal, Modal,
Pressable Pressable
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { StreamingContent, CatalogContent, catalogService } from '../services/catalogService'; import { StreamingContent, CatalogContent, catalogService } from '../services/catalogService';
@ -74,6 +74,10 @@ interface DropUpMenuProps {
onOptionSelect: (option: string) => void; onOptionSelect: (option: string) => void;
} }
interface ContinueWatchingRef {
refresh: () => Promise<boolean>;
}
const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => {
const translateY = useSharedValue(300); const translateY = useSharedValue(300);
const opacity = useSharedValue(0); const opacity = useSharedValue(0);
@ -354,11 +358,12 @@ const SkeletonFeatured = () => (
const HomeScreen = () => { const HomeScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
const continueWatchingRef = useRef<{ refresh: () => Promise<void> }>(null); const continueWatchingRef = useRef<ContinueWatchingRef>(null);
const { settings } = useSettings(); const { settings } = useSettings();
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection); const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource); const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource);
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null); const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [hasContinueWatching, setHasContinueWatching] = useState(false);
const { const {
catalogs, catalogs,
@ -408,9 +413,25 @@ const HomeScreen = () => {
}; };
}, [featuredContentSource, showHeroSection, refreshFeatured]); }, [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(() => { useEffect(() => {
StatusBar.setTranslucent(true); // Only run cleanup when component unmounts completely, not on unfocus
StatusBar.setBackgroundColor('transparent');
return () => { return () => {
StatusBar.setTranslucent(false); StatusBar.setTranslucent(false);
StatusBar.setBackgroundColor(colors.darkBackground); StatusBar.setBackgroundColor(colors.darkBackground);
@ -484,9 +505,10 @@ const HomeScreen = () => {
}); });
}, [featuredContent, navigation]); }, [featuredContent, navigation]);
const refreshContinueWatching = useCallback(() => { const refreshContinueWatching = useCallback(async () => {
if (continueWatchingRef.current) { if (continueWatchingRef.current) {
continueWatchingRef.current.refresh(); const hasContent = await continueWatchingRef.current.refresh();
setHasContinueWatching(hasContent);
} }
}, []); }, []);
@ -695,7 +717,7 @@ const HomeScreen = () => {
} }
return ( return (
<View style={[styles.container]}> <SafeAreaView style={[styles.container]}>
<StatusBar <StatusBar
barStyle="light-content" barStyle="light-content"
backgroundColor="transparent" backgroundColor="transparent"
@ -710,7 +732,10 @@ const HomeScreen = () => {
colors={[colors.primary, colors.secondary]} colors={[colors.primary, colors.secondary]}
/> />
} }
contentContainerStyle={styles.scrollContent} contentContainerStyle={[
styles.scrollContent,
{ paddingTop: Platform.OS === 'ios' ? 0 : 0 }
]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
{showHeroSection && renderFeaturedContent()} {showHeroSection && renderFeaturedContent()}
@ -719,9 +744,11 @@ const HomeScreen = () => {
<ThisWeekSection /> <ThisWeekSection />
</Animated.View> </Animated.View>
<Animated.View entering={FadeIn.duration(400).delay(250)}> {hasContinueWatching && (
<ContinueWatchingSection ref={continueWatchingRef} /> <Animated.View entering={FadeIn.duration(400).delay(250)}>
</Animated.View> <ContinueWatchingSection ref={continueWatchingRef} />
</Animated.View>
)}
{catalogs.length > 0 ? ( {catalogs.length > 0 ? (
catalogs.map((catalog, index) => ( catalogs.map((catalog, index) => (
@ -747,7 +774,7 @@ const HomeScreen = () => {
) )
)} )}
</ScrollView> </ScrollView>
</View> </SafeAreaView>
); );
}; };
@ -770,7 +797,7 @@ const styles = StyleSheet.create<any>({
featuredContainer: { featuredContainer: {
width: '100%', width: '100%',
height: height * 0.6, height: height * 0.6,
marginTop: Platform.OS === 'ios' ? 85 : 75, marginTop: Platform.OS === 'ios' ? 0 : 0,
marginBottom: 8, marginBottom: 8,
position: 'relative', position: 'relative',
}, },

View file

@ -11,11 +11,9 @@ import {
ScrollView, ScrollView,
StatusBar, StatusBar,
Platform, Platform,
Linking
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import * as WebBrowser from 'expo-web-browser'; import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session';
import { makeRedirectUri } from 'expo-auth-session';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { traktService, TraktUser } from '../services/traktService'; import { traktService, TraktUser } from '../services/traktService';
import { colors } from '../styles/colors'; import { colors } from '../styles/colors';
@ -25,6 +23,14 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; 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 // For use with deep linking
const redirectUri = makeRedirectUri({ const redirectUri = makeRedirectUri({
scheme: 'stremioexpo', scheme: 'stremioexpo',
@ -36,7 +42,6 @@ const TraktSettingsScreen: React.FC = () => {
const isDarkMode = settings.enableDarkMode; const isDarkMode = settings.enableDarkMode;
const navigation = useNavigation(); const navigation = useNavigation();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userProfile, setUserProfile] = useState<TraktUser | null>(null); const [userProfile, setUserProfile] = useState<TraktUser | null>(null);
@ -49,6 +54,8 @@ const TraktSettingsScreen: React.FC = () => {
if (authenticated) { if (authenticated) {
const profile = await traktService.getUserProfile(); const profile = await traktService.getUserProfile();
setUserProfile(profile); setUserProfile(profile);
} else {
setUserProfile(null); // Ensure profile is cleared if not authenticated
} }
} catch (error) { } catch (error) {
logger.error('[TraktSettingsScreen] Error checking auth status:', error); logger.error('[TraktSettingsScreen] Error checking auth status:', error);
@ -61,45 +68,57 @@ const TraktSettingsScreen: React.FC = () => {
checkAuthStatus(); checkAuthStatus();
}, [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(() => { useEffect(() => {
const handleRedirect = async (event: { url: string }) => { if (response) {
const { url } = event; setIsExchangingCode(true); // Indicate we're processing the response
if (url.includes('auth/trakt')) { if (response.type === 'success') {
setIsAuthenticating(true); const { code } = response.params;
try { logger.log('[TraktSettingsScreen] Auth code received:', code);
const code = url.split('code=')[1].split('&')[0]; traktService.exchangeCodeForToken(code)
const success = await traktService.exchangeCodeForToken(code); .then(success => {
if (success) { if (success) {
checkAuthStatus(); logger.log('[TraktSettingsScreen] Token exchange successful');
} else { checkAuthStatus(); // Re-check auth status and fetch profile
Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.'); } else {
} logger.error('[TraktSettingsScreen] Token exchange failed');
} catch (error) { Alert.alert('Authentication Error', 'Failed to complete authentication with Trakt.');
logger.error('[TraktSettingsScreen] Authentication error:', error); }
Alert.alert('Authentication Error', 'An error occurred during authentication.'); })
} finally { .catch(error => {
setIsAuthenticating(false); 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 () => { const handleSignOut = async () => {
@ -249,9 +268,9 @@ const TraktSettingsScreen: React.FC = () => {
{ backgroundColor: isDarkMode ? colors.primary : colors.primary } { backgroundColor: isDarkMode ? colors.primary : colors.primary }
]} ]}
onPress={handleSignIn} onPress={handleSignIn}
disabled={isAuthenticating} disabled={!request || isExchangingCode} // Disable while waiting for response or exchanging code
> >
{isAuthenticating ? ( {isExchangingCode ? (
<ActivityIndicator size="small" color="white" /> <ActivityIndicator size="small" color="white" />
) : ( ) : (
<Text style={styles.buttonText}> <Text style={styles.buttonText}>