mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-23 01:32:11 +00:00
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",
|
"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",
|
||||||
|
|
|
||||||
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;
|
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);
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue