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",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"scheme": "stremioexpo",
"newArchEnabled": true,
"splash": {
"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;
}
// Define the ref interface
interface ContinueWatchingRef {
refresh: () => Promise<boolean>;
}
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<void> }>((props, ref) => {
// Create a proper imperative handle with React.forwardRef and updated type
const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const [continueWatchingItems, setContinueWatchingItems] = useState<ContinueWatchingItem[]>([]);
const [loading, setLoading] = useState(true);
@ -188,7 +193,11 @@ const ContinueWatchingSection = React.forwardRef<{ refresh: () => Promise<void>
// 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);

View file

@ -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<boolean>;
}
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<NavigationProp<RootStackParamList>>();
const isDarkMode = useColorScheme() === 'dark';
const continueWatchingRef = useRef<{ refresh: () => Promise<void> }>(null);
const continueWatchingRef = useRef<ContinueWatchingRef>(null);
const { settings } = useSettings();
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource);
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(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 (
<View style={[styles.container]}>
<SafeAreaView style={[styles.container]}>
<StatusBar
barStyle="light-content"
backgroundColor="transparent"
@ -710,7 +732,10 @@ const HomeScreen = () => {
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 = () => {
<ThisWeekSection />
</Animated.View>
<Animated.View entering={FadeIn.duration(400).delay(250)}>
<ContinueWatchingSection ref={continueWatchingRef} />
</Animated.View>
{hasContinueWatching && (
<Animated.View entering={FadeIn.duration(400).delay(250)}>
<ContinueWatchingSection ref={continueWatchingRef} />
</Animated.View>
)}
{catalogs.length > 0 ? (
catalogs.map((catalog, index) => (
@ -747,7 +774,7 @@ const HomeScreen = () => {
)
)}
</ScrollView>
</View>
</SafeAreaView>
);
};
@ -770,7 +797,7 @@ const styles = StyleSheet.create<any>({
featuredContainer: {
width: '100%',
height: height * 0.6,
marginTop: Platform.OS === 'ios' ? 85 : 75,
marginTop: Platform.OS === 'ios' ? 0 : 0,
marginBottom: 8,
position: 'relative',
},

View file

@ -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<TraktUser | null>(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 ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text style={styles.buttonText}>