major changes

This commit is contained in:
Nayif Noushad 2025-04-15 22:12:37 +05:30
parent b10b3479d7
commit 08af55edbb
6 changed files with 1575 additions and 1063 deletions

36
package-lock.json generated
View file

@ -36,9 +36,11 @@
"lodash": "^4.17.21",
"react": "18.3.1",
"react-native": "0.76.9",
"react-native-awesome-slider": "^2.9.0",
"react-native-gesture-handler": "~2.20.2",
"react-native-immersive-mode": "^2.0.2",
"react-native-modal": "^14.0.0-rc.1",
"react-native-orientation-locker": "^1.7.0",
"react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
@ -10557,6 +10559,24 @@
"prop-types": "^15.8.1"
}
},
"node_modules/react-native-awesome-slider": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-native-awesome-slider/-/react-native-awesome-slider-2.9.0.tgz",
"integrity": "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ==",
"license": "MIT",
"workspaces": [
"example"
],
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-gesture-handler": ">=2.0.0",
"react-native-reanimated": ">=3.0.0"
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.20.2",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
@ -10595,6 +10615,22 @@
"react-native": ">=0.70.0"
}
},
"node_modules/react-native-orientation-locker": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/react-native-orientation-locker/-/react-native-orientation-locker-1.7.0.tgz",
"integrity": "sha512-2PhG4UyRJktb3KCTISStuu8/q+Q3q3oPesGg9DhdY0b6Cu/ZzxkCvkbJte2TPWRYkS0JpClimvqVaonulGvDrA==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.13.1",
"react-native": ">=0.63.2",
"react-native-windows": ">=0.63.3"
},
"peerDependenciesMeta": {
"react-native-windows": {
"optional": true
}
}
},
"node_modules/react-native-paper": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.13.1.tgz",

View file

@ -37,9 +37,11 @@
"lodash": "^4.17.21",
"react": "18.3.1",
"react-native": "0.76.9",
"react-native-awesome-slider": "^2.9.0",
"react-native-gesture-handler": "~2.20.2",
"react-native-immersive-mode": "^2.0.2",
"react-native-modal": "^14.0.0-rc.1",
"react-native-orientation-locker": "^1.7.0",
"react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",

View file

@ -0,0 +1,367 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
Dimensions,
AppState,
AppStateStatus
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
import { StreamingContent, catalogService } from '../../services/catalogService';
import { LinearGradient } from 'expo-linear-gradient';
import { Image as ExpoImage } from 'expo-image';
import { colors } from '../../styles/colors';
import { storageService } from '../../services/storageService';
import { logger } from '../../utils/logger';
// Define interface for continue watching items
interface ContinueWatchingItem extends StreamingContent {
progress: number;
lastUpdated: number;
season?: number;
episode?: number;
episodeTitle?: string;
}
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) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const [continueWatchingItems, setContinueWatchingItems] = useState<ContinueWatchingItem[]>([]);
const [loading, setLoading] = useState(true);
const appState = useRef(AppState.currentState);
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
// Modified loadContinueWatching to be more efficient
const loadContinueWatching = useCallback(async () => {
try {
setLoading(true);
const allProgress = await storageService.getAllWatchProgress();
if (Object.keys(allProgress).length === 0) {
setContinueWatchingItems([]);
return;
}
const progressItems: ContinueWatchingItem[] = [];
const latestEpisodes: Record<string, ContinueWatchingItem> = {};
const contentPromises: Promise<void>[] = [];
// Process each saved progress
for (const key in allProgress) {
// Parse the key to get type and id
const [type, id, episodeId] = key.split(':');
const progress = allProgress[key];
// Skip items that are more than 95% complete (effectively finished)
const progressPercent = (progress.currentTime / progress.duration) * 100;
if (progressPercent >= 95) continue;
const contentPromise = (async () => {
try {
let content: StreamingContent | null = null;
// Get content details using catalogService
content = await catalogService.getContentDetails(type, id);
if (content) {
// Extract season and episode info from episodeId if available
let season: number | undefined;
let episode: number | undefined;
let episodeTitle: string | undefined;
if (episodeId && type === 'series') {
const match = episodeId.match(/s(\d+)e(\d+)/i);
if (match) {
season = parseInt(match[1], 10);
episode = parseInt(match[2], 10);
episodeTitle = `Episode ${episode}`;
}
}
const continueWatchingItem: ContinueWatchingItem = {
...content,
progress: progressPercent,
lastUpdated: progress.lastUpdated,
season,
episode,
episodeTitle
};
if (type === 'series') {
// For series, keep only the latest watched episode for each show
if (!latestEpisodes[id] || latestEpisodes[id].lastUpdated < progress.lastUpdated) {
latestEpisodes[id] = continueWatchingItem;
}
} else {
// For movies, add to the list directly
progressItems.push(continueWatchingItem);
}
}
} catch (error) {
logger.error(`Failed to get content details for ${type}:${id}`, error);
}
})();
contentPromises.push(contentPromise);
}
// Wait for all content to be processed
await Promise.all(contentPromises);
// Add the latest episodes for each series to the items list
progressItems.push(...Object.values(latestEpisodes));
// Sort by last updated time (most recent first)
progressItems.sort((a, b) => b.lastUpdated - a.lastUpdated);
// Limit to 10 items
setContinueWatchingItems(progressItems.slice(0, 10));
} catch (error) {
logger.error('Failed to load continue watching items:', error);
} finally {
setLoading(false);
}
}, []);
// Function to handle app state changes
const handleAppStateChange = useCallback((nextAppState: AppStateStatus) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
// App has come to the foreground - refresh data
loadContinueWatching();
}
appState.current = nextAppState;
}, [loadContinueWatching]);
// Set up storage event listener and app state listener
useEffect(() => {
// Add app state change listener
const subscription = AppState.addEventListener('change', handleAppStateChange);
// Add custom event listener for watch progress updates
const watchProgressUpdateHandler = () => {
// Debounce updates to avoid too frequent refreshes
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
refreshTimerRef.current = setTimeout(() => {
loadContinueWatching();
}, 300);
};
// Try to set up a custom event listener or use a timer as fallback
if (storageService.subscribeToWatchProgressUpdates) {
const unsubscribe = storageService.subscribeToWatchProgressUpdates(watchProgressUpdateHandler);
return () => {
subscription.remove();
unsubscribe();
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
};
} else {
// Fallback: poll for updates every 30 seconds
const intervalId = setInterval(loadContinueWatching, 30000);
return () => {
subscription.remove();
clearInterval(intervalId);
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
};
}
}, [loadContinueWatching, handleAppStateChange]);
// Initial load
useEffect(() => {
loadContinueWatching();
}, [loadContinueWatching]);
// Properly expose the refresh method
React.useImperativeHandle(ref, () => ({
refresh: loadContinueWatching
}));
const handleContentPress = useCallback((id: string, type: string) => {
navigation.navigate('Metadata', { id, type });
}, [navigation]);
// If no continue watching items, don't render anything
if (continueWatchingItems.length === 0 && !loading) {
return null;
}
return (
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={styles.title}>Continue Watching</Text>
<LinearGradient
colors={[colors.primary, colors.secondary]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.titleUnderline}
/>
</View>
</View>
<FlatList
data={continueWatchingItems}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.contentItem}
activeOpacity={0.7}
onPress={() => handleContentPress(item.id, item.type)}
>
<View style={styles.contentItemContainer}>
<ExpoImage
source={{ uri: item.poster }}
style={styles.poster}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
{item.type === 'series' && item.season && item.episode && (
<View style={styles.episodeInfoContainer}>
<Text style={styles.episodeInfo}>
S{item.season.toString().padStart(2, '0')}E{item.episode.toString().padStart(2, '0')}
</Text>
{item.episodeTitle && (
<Text style={styles.episodeTitle} numberOfLines={1}>
{item.episodeTitle}
</Text>
)}
</View>
)}
{/* Progress bar indicator */}
<View style={styles.progressBarContainer}>
<View
style={[
styles.progressBar,
{ width: `${item.progress}%` }
]}
/>
</View>
</View>
</TouchableOpacity>
)}
keyExtractor={(item) => `continue-${item.id}-${item.type}`}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.list}
snapToInterval={POSTER_WIDTH + 10}
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
/>
</View>
);
});
const styles = StyleSheet.create({
container: {
marginBottom: 24,
paddingTop: 0,
marginTop: 12,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 8,
},
titleContainer: {
position: 'relative',
},
title: {
fontSize: 18,
fontWeight: '800',
color: colors.highEmphasis,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 6,
},
titleUnderline: {
position: 'absolute',
bottom: -4,
left: 0,
width: 60,
height: 3,
borderRadius: 1.5,
},
list: {
paddingHorizontal: 16,
paddingBottom: 8,
paddingTop: 4,
},
contentItem: {
width: POSTER_WIDTH,
aspectRatio: 2/3,
margin: 0,
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
contentItemContainer: {
width: '100%',
height: '100%',
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
},
poster: {
width: '100%',
height: '100%',
borderRadius: 12,
},
episodeInfoContainer: {
position: 'absolute',
bottom: 3,
left: 0,
right: 0,
padding: 4,
paddingHorizontal: 8,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
},
episodeInfo: {
fontSize: 12,
fontWeight: 'bold',
color: colors.white,
},
episodeTitle: {
fontSize: 10,
color: colors.white,
opacity: 0.9,
},
progressBarContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 3,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
progressBar: {
height: '100%',
backgroundColor: colors.primary,
},
});
export default React.memo(ContinueWatchingSection);

View file

@ -47,9 +47,11 @@ import {
} from 'react-native-gesture-handler';
import { useCatalogContext } from '../contexts/CatalogContext';
import { ThisWeekSection } from '../components/home/ThisWeekSection';
import ContinueWatchingSection from '../components/home/ContinueWatchingSection';
import * as Haptics from 'expo-haptics';
import { tmdbService } from '../services/tmdbService';
import { logger } from '../utils/logger';
import { storageService } from '../services/storageService';
// Define interfaces for our data
interface Category {
@ -407,6 +409,7 @@ const HomeScreen = () => {
const [isSaved, setIsSaved] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const currentIndexRef = useRef(0);
const continueWatchingRef = useRef<{ refresh: () => Promise<void> }>(null);
// Add auto-rotation effect
useEffect(() => {
@ -563,24 +566,27 @@ const HomeScreen = () => {
}
}, [maxRetries, cleanup]);
useEffect(() => {
const loadInitialData = async () => {
setLoading(true);
try {
await Promise.all([
loadFeaturedContent(),
loadCatalogs(),
]);
} catch (error) {
logger.error('Error loading initial data:', error);
} finally {
setLoading(false);
}
};
// Update loadInitialData to remove continue watching loading
const loadInitialData = async () => {
setLoading(true);
try {
await Promise.all([
loadFeaturedContent(),
loadCatalogs(),
]);
} catch (error) {
logger.error('Error loading initial data:', error);
} finally {
setLoading(false);
}
};
// Add back the useEffect for loadInitialData
useEffect(() => {
loadInitialData();
}, [loadFeaturedContent, loadCatalogs, lastUpdate]);
// Update handleRefresh to remove continue watching loading
const handleRefresh = useCallback(() => {
setRefreshing(true);
Promise.all([
@ -653,6 +659,30 @@ const HomeScreen = () => {
});
}, [featuredContent, navigation]);
// Add a function to refresh the Continue Watching section
const refreshContinueWatching = useCallback(() => {
if (continueWatchingRef.current) {
continueWatchingRef.current.refresh();
}
}, []);
// Update the event listener for video playback completion
useEffect(() => {
const handlePlaybackComplete = () => {
refreshContinueWatching();
};
// Listen for playback complete events
const unsubscribe = navigation.addListener('focus', () => {
// When returning to HomeScreen, refresh Continue Watching
refreshContinueWatching();
});
return () => {
unsubscribe();
};
}, [navigation, refreshContinueWatching]);
const renderFeaturedContent = () => {
if (!featuredContent) {
return <SkeletonFeatured />;
@ -854,6 +884,9 @@ const HomeScreen = () => {
{/* This Week Section */}
<ThisWeekSection />
{/* Continue Watching Section */}
<ContinueWatchingSection ref={continueWatchingRef} />
{/* Catalogs */}
{catalogs.length > 0 ? (
<FlatList

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@ interface WatchProgress {
class StorageService {
private static instance: StorageService;
private readonly WATCH_PROGRESS_KEY = '@watch_progress:';
private watchProgressSubscribers: (() => void)[] = [];
private constructor() {}
@ -33,11 +34,29 @@ class StorageService {
try {
const key = this.getWatchProgressKey(id, type, episodeId);
await AsyncStorage.setItem(key, JSON.stringify(progress));
// Notify subscribers
this.notifyWatchProgressSubscribers();
} catch (error) {
logger.error('Error saving watch progress:', error);
}
}
private notifyWatchProgressSubscribers(): void {
this.watchProgressSubscribers.forEach(callback => callback());
}
public subscribeToWatchProgressUpdates(callback: () => void): () => void {
this.watchProgressSubscribers.push(callback);
// Return unsubscribe function
return () => {
const index = this.watchProgressSubscribers.indexOf(callback);
if (index > -1) {
this.watchProgressSubscribers.splice(index, 1);
}
};
}
public async getWatchProgress(
id: string,
type: string,
@ -61,6 +80,8 @@ class StorageService {
try {
const key = this.getWatchProgressKey(id, type, episodeId);
await AsyncStorage.removeItem(key);
// Notify subscribers
this.notifyWatchProgressSubscribers();
} catch (error) {
logger.error('Error removing watch progress:', error);
}