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:
parent
c95d9d8093
commit
b1e1017288
7 changed files with 123 additions and 58 deletions
1
app.json
1
app.json
|
|
@ -6,6 +6,7 @@
|
|||
"orientation": "default",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"scheme": "stremioexpo",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
|
|
|
|||
BIN
assets/WhatsApp Image 2025-04-24 at 09.47.46_1d72ceb0.jpg
Normal file
BIN
assets/WhatsApp Image 2025-04-24 at 09.47.46_1d72ceb0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
BIN
src/assets/Desktop (1).png
Normal file
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 |
|
|
@ -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);
|
||||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue