Enhance ContinueWatchingSection with improved logging, validation, and UI updates

This update introduces a validation function for IMDB IDs, enhancing the robustness of the ContinueWatchingSection. It also adds detailed logging throughout the loading process, providing insights into the progress items being processed and any filtering based on completion percentage. The UI has been refined with new styles for content items, including a wider layout and improved progress display, ensuring a better user experience. Additionally, the HomeScreen has been updated to include debug buttons for managing watch progress, facilitating easier testing and interaction.
This commit is contained in:
tapframe 2025-06-20 01:25:14 +05:30
parent 7fb168f530
commit 6c2259f6e8
4 changed files with 1555 additions and 58 deletions

1165
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ import {
AppState,
AppStateStatus
} from 'react-native';
import Animated, { FadeIn } from 'react-native-reanimated';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
@ -60,6 +61,13 @@ const { width } = Dimensions.get('window');
const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth;
// Function to validate IMDB ID format
const isValidImdbId = (id: string): boolean => {
// IMDB IDs should start with 'tt' followed by 7-10 digits
const imdbPattern = /^tt\d{7,10}$/;
return imdbPattern.test(id);
};
// Create a proper imperative handle with React.forwardRef and updated type
const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -72,9 +80,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Modified loadContinueWatching to be more efficient
const loadContinueWatching = useCallback(async () => {
try {
console.log('[ContinueWatching] Starting to load continue watching items...');
setLoading(true);
const allProgress = await storageService.getAllWatchProgress();
console.log(`[ContinueWatching] Found ${Object.keys(allProgress).length} progress items in storage`);
if (Object.keys(allProgress).length === 0) {
console.log('[ContinueWatching] No progress items found, setting empty array');
setContinueWatchingItems([]);
return;
}
@ -85,33 +97,80 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Process each saved progress
for (const key in allProgress) {
console.log(`[ContinueWatching] Raw key from storage: "${key}"`);
// Parse the key to get type and id
const [type, id, episodeId] = key.split(':');
const keyParts = key.split(':');
console.log(`[ContinueWatching] Key parts:`, keyParts);
const [type, id, ...episodeIdParts] = keyParts;
const episodeId = episodeIdParts.length > 0 ? episodeIdParts.join(':') : undefined;
const progress = allProgress[key];
console.log(`[ContinueWatching] Parsed - type: "${type}", id: "${id}", episodeId: "${episodeId}"`);
// Skip items that are more than 95% complete (effectively finished)
const progressPercent = (progress.currentTime / progress.duration) * 100;
if (progressPercent >= 95) continue;
console.log(`[ContinueWatching] Progress for ${key}: ${progressPercent.toFixed(1)}%`);
if (progressPercent >= 95) {
console.log(`[ContinueWatching] Skipping ${key} - too high progress (${progressPercent.toFixed(1)}%)`);
continue;
}
const contentPromise = (async () => {
try {
// Validate IMDB ID format before attempting to fetch
if (!isValidImdbId(id)) {
console.log(`[ContinueWatching] Skipping ${type}:${id} - invalid IMDB ID format`);
return;
}
console.log(`[ContinueWatching] Fetching content details for ${type}:${id}`);
let content: StreamingContent | null = null;
// Get content details using catalogService
content = await catalogService.getContentDetails(type, id);
if (content) {
console.log(`[ContinueWatching] Successfully fetched content: ${content.name}`);
// 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);
console.log(`[ContinueWatching] Parsing episode ID: ${episodeId}`);
// Try different episode ID formats
let match = episodeId.match(/s(\d+)e(\d+)/i); // Format: s1e1
if (match) {
season = parseInt(match[1], 10);
episode = parseInt(match[2], 10);
episodeTitle = `Episode ${episode}`;
console.log(`[ContinueWatching] Parsed s1e1 format: S${season}E${episode}`);
} else {
// Try format: seriesId:season:episode (e.g., tt0108778:4:6)
const parts = episodeId.split(':');
if (parts.length >= 3) {
const seasonPart = parts[parts.length - 2]; // Second to last part
const episodePart = parts[parts.length - 1]; // Last part
const seasonNum = parseInt(seasonPart, 10);
const episodeNum = parseInt(episodePart, 10);
if (!isNaN(seasonNum) && !isNaN(episodeNum)) {
season = seasonNum;
episode = episodeNum;
episodeTitle = `Episode ${episode}`;
console.log(`[ContinueWatching] Parsed colon format: S${season}E${episode}`);
}
}
}
if (!season || !episode) {
console.log(`[ContinueWatching] Failed to parse episode details from: ${episodeId}`);
}
}
@ -124,18 +183,31 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
episodeTitle
};
console.log(`[ContinueWatching] Created item for ${content.name}:`, {
type,
season,
episode,
episodeTitle,
episodeId,
originalKey: key
});
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;
console.log(`[ContinueWatching] Updated latest episode for series ${id}`);
}
} else {
// For movies, add to the list directly
progressItems.push(continueWatchingItem);
console.log(`[ContinueWatching] Added movie to progress items`);
}
} else {
console.log(`[ContinueWatching] Failed to fetch content details for ${type}:${id}`);
}
} catch (error) {
logger.error(`Failed to get content details for ${type}:${id}`, error);
console.error(`[ContinueWatching] Failed to get content details for ${type}:${id}`, error);
}
})();
@ -143,18 +215,35 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
}
// Wait for all content to be processed
console.log(`[ContinueWatching] Waiting for ${contentPromises.length} content promises...`);
await Promise.all(contentPromises);
// Add the latest episodes for each series to the items list
progressItems.push(...Object.values(latestEpisodes));
console.log(`[ContinueWatching] Total items after processing: ${progressItems.length}`);
// 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));
const finalItems = progressItems.slice(0, 10);
console.log(`[ContinueWatching] Final continue watching items: ${finalItems.length}`);
// Debug: Log the final items with their episode details
finalItems.forEach((item, index) => {
console.log(`[ContinueWatching] Item ${index}:`, {
name: item.name,
type: item.type,
season: item.season,
episode: item.episode,
episodeTitle: item.episodeTitle,
progress: item.progress
});
});
setContinueWatchingItems(finalItems);
} catch (error) {
logger.error('Failed to load continue watching items:', error);
console.error('[ContinueWatching] Failed to load continue watching items:', error);
} finally {
setLoading(false);
}
@ -219,9 +308,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Properly expose the refresh method
React.useImperativeHandle(ref, () => ({
refresh: async () => {
console.log('[ContinueWatching] Refresh method called');
await loadContinueWatching();
// Return whether there are items to help parent determine visibility
return continueWatchingItems.length > 0;
const hasItems = continueWatchingItems.length > 0;
console.log(`[ContinueWatching] Refresh returning hasItems: ${hasItems}, items count: ${continueWatchingItems.length}`);
return hasItems;
}
}));
@ -235,7 +327,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
}
return (
<View style={styles.container}>
<Animated.View entering={FadeIn.duration(400).delay(250)} style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>Continue Watching</Text>
@ -252,41 +344,91 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
data={continueWatchingItems}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.contentItem, {
style={[styles.wideContentItem, {
backgroundColor: currentTheme.colors.elevation1,
borderColor: currentTheme.colors.border,
shadowColor: currentTheme.colors.black
}]}
activeOpacity={0.7}
activeOpacity={0.8}
onPress={() => handleContentPress(item.id, item.type)}
>
<View style={styles.contentItemContainer}>
{/* Poster Image */}
<View style={styles.posterContainer}>
<ExpoImage
source={{ uri: item.poster }}
style={styles.poster}
style={styles.widePoster}
contentFit="cover"
transition={200}
cachePolicy="memory-disk"
/>
{item.type === 'series' && item.season && item.episode && (
<View style={[styles.episodeInfoContainer, { backgroundColor: 'rgba(0, 0, 0, 0.7)' }]}>
<Text style={[styles.episodeInfo, { color: currentTheme.colors.white }]}>
S{item.season.toString().padStart(2, '0')}E{item.episode.toString().padStart(2, '0')}
</Text>
{item.episodeTitle && (
<Text style={[styles.episodeTitle, { color: currentTheme.colors.white, opacity: 0.9 }]} numberOfLines={1}>
{item.episodeTitle}
</Text>
)}
</View>
{/* Content Details */}
<View style={styles.contentDetails}>
<View style={styles.titleRow}>
<Text
style={[styles.contentTitle, { color: currentTheme.colors.highEmphasis }]}
numberOfLines={1}
>
{item.name}
</Text>
<View style={[styles.progressBadge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.progressText}>{Math.round(item.progress)}%</Text>
</View>
)}
{/* Progress bar indicator */}
<View style={styles.progressBarContainer}>
<View
style={[
styles.progressBar,
{ width: `${item.progress}%`, backgroundColor: currentTheme.colors.primary }
]}
/>
</View>
{/* Episode Info or Year */}
{(() => {
console.log(`[ContinueWatching] Rendering item:`, {
name: item.name,
type: item.type,
season: item.season,
episode: item.episode,
episodeTitle: item.episodeTitle,
hasSeasonAndEpisode: !!(item.season && item.episode)
});
if (item.type === 'series' && item.season && item.episode) {
return (
<View style={styles.episodeRow}>
<Text style={[styles.episodeText, { color: currentTheme.colors.mediumEmphasis }]}>
Season {item.season}
</Text>
{item.episodeTitle && (
<Text
style={[styles.episodeTitle, { color: currentTheme.colors.mediumEmphasis }]}
numberOfLines={1}
>
{item.episodeTitle}
</Text>
)}
</View>
);
} else {
return (
<Text style={[styles.yearText, { color: currentTheme.colors.mediumEmphasis }]}>
{item.year} {item.type === 'movie' ? 'Movie' : 'Series'}
</Text>
);
}
})()}
{/* Progress Bar */}
<View style={styles.wideProgressContainer}>
<View style={styles.wideProgressTrack}>
<View
style={[
styles.wideProgressBar,
{
width: `${item.progress}%`,
backgroundColor: currentTheme.colors.primary
}
]}
/>
</View>
<Text style={[styles.progressLabel, { color: currentTheme.colors.textMuted }]}>
{Math.round(item.progress)}% watched
</Text>
</View>
</View>
</TouchableOpacity>
@ -294,13 +436,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
keyExtractor={(item) => `continue-${item.id}-${item.type}`}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.list}
snapToInterval={POSTER_WIDTH + 10}
contentContainerStyle={styles.wideList}
snapToInterval={280 + 16} // Card width + margin
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
/>
</View>
</Animated.View>
);
});
@ -315,7 +457,7 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 8,
marginBottom: 12,
},
titleContainer: {
position: 'relative',
@ -335,6 +477,96 @@ const styles = StyleSheet.create({
height: 3,
borderRadius: 1.5,
},
wideList: {
paddingHorizontal: 16,
paddingBottom: 8,
paddingTop: 4,
},
wideContentItem: {
width: 280,
height: 120,
flexDirection: 'row',
borderRadius: 12,
overflow: 'hidden',
elevation: 6,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.2,
shadowRadius: 6,
borderWidth: 1,
},
posterContainer: {
width: 80,
height: '100%',
},
widePoster: {
width: '100%',
height: '100%',
borderTopLeftRadius: 12,
borderBottomLeftRadius: 12,
},
contentDetails: {
flex: 1,
padding: 12,
justifyContent: 'space-between',
},
titleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 4,
},
contentTitle: {
fontSize: 16,
fontWeight: '700',
flex: 1,
marginRight: 8,
},
progressBadge: {
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 12,
minWidth: 44,
alignItems: 'center',
},
progressText: {
fontSize: 12,
fontWeight: '700',
color: '#FFFFFF',
},
episodeRow: {
marginBottom: 8,
},
episodeText: {
fontSize: 13,
fontWeight: '600',
marginBottom: 2,
},
episodeTitle: {
fontSize: 12,
},
yearText: {
fontSize: 13,
fontWeight: '500',
marginBottom: 8,
},
wideProgressContainer: {
marginTop: 'auto',
},
wideProgressTrack: {
height: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 2,
marginBottom: 4,
},
wideProgressBar: {
height: '100%',
borderRadius: 2,
},
progressLabel: {
fontSize: 11,
fontWeight: '500',
},
// Keep old styles for backward compatibility
list: {
paddingHorizontal: 16,
paddingBottom: 8,
@ -377,9 +609,6 @@ const styles = StyleSheet.create({
fontSize: 12,
fontWeight: 'bold',
},
episodeTitle: {
fontSize: 10,
},
progressBarContainer: {
position: 'absolute',
bottom: 0,

View file

@ -488,7 +488,7 @@ const HomeScreen = () => {
await Promise.all(imagePromises);
} catch (error) {
console.error('Error preloading images:', error);
// Silently handle preload errors
}
}, []);
@ -530,9 +530,37 @@ const HomeScreen = () => {
}, [featuredContent, navigation]);
const refreshContinueWatching = useCallback(async () => {
console.log('[HomeScreen] Refreshing continue watching...');
if (continueWatchingRef.current) {
const hasContent = await continueWatchingRef.current.refresh();
setHasContinueWatching(hasContent);
try {
const hasContent = await continueWatchingRef.current.refresh();
console.log(`[HomeScreen] Continue watching has content: ${hasContent}`);
setHasContinueWatching(hasContent);
// Debug: Let's check what's in storage
const allProgress = await storageService.getAllWatchProgress();
console.log('[HomeScreen] All watch progress in storage:', Object.keys(allProgress).length, 'items');
console.log('[HomeScreen] Watch progress items:', allProgress);
// Check if any items are being filtered out due to >95% progress
let filteredCount = 0;
for (const [key, progress] of Object.entries(allProgress)) {
const progressPercent = (progress.currentTime / progress.duration) * 100;
if (progressPercent >= 95) {
filteredCount++;
console.log(`[HomeScreen] Filtered out ${key}: ${progressPercent.toFixed(1)}% complete`);
} else {
console.log(`[HomeScreen] Valid progress ${key}: ${progressPercent.toFixed(1)}% complete`);
}
}
console.log(`[HomeScreen] Filtered out ${filteredCount} completed items`);
} catch (error) {
console.error('[HomeScreen] Error refreshing continue watching:', error);
setHasContinueWatching(false);
}
} else {
console.log('[HomeScreen] Continue watching ref is null');
}
}, []);
@ -596,11 +624,50 @@ const HomeScreen = () => {
<ThisWeekSection />
</Animated.View>
{hasContinueWatching && (
<Animated.View entering={FadeIn.duration(400).delay(250)}>
<ContinueWatchingSection ref={continueWatchingRef} />
</Animated.View>
)}
{/* Debug buttons for Continue Watching */}
<View style={{ flexDirection: 'row', padding: 16, gap: 10 }}>
<TouchableOpacity
style={{
backgroundColor: currentTheme.colors.primary,
padding: 10,
borderRadius: 8,
flex: 1
}}
onPress={addTestWatchProgress}
>
<Text style={{ color: 'white', textAlign: 'center', fontSize: 12 }}>
Add Test Progress
</Text>
</TouchableOpacity>
<TouchableOpacity
style={{
backgroundColor: currentTheme.colors.error || '#ff4444',
padding: 10,
borderRadius: 8,
flex: 1
}}
onPress={clearAllWatchProgress}
>
<Text style={{ color: 'white', textAlign: 'center', fontSize: 12 }}>
Clear All Progress
</Text>
</TouchableOpacity>
<TouchableOpacity
style={{
backgroundColor: currentTheme.colors.secondary,
padding: 10,
borderRadius: 8,
flex: 1
}}
onPress={refreshContinueWatching}
>
<Text style={{ color: 'white', textAlign: 'center', fontSize: 12 }}>
Refresh
</Text>
</TouchableOpacity>
</View>
<ContinueWatchingSection ref={continueWatchingRef} />
{catalogs.length > 0 ? (
catalogs.map((catalog, index) => (
@ -642,6 +709,58 @@ const HomeScreen = () => {
featuredContentSource
]);
// Debug function to add test watch progress
const addTestWatchProgress = useCallback(async () => {
console.log('[HomeScreen] Adding test watch progress data...');
try {
// Add a test movie with 50% progress
await storageService.setWatchProgress(
'tt1375666', // Inception IMDB ID
'movie',
{
currentTime: 3600, // 1 hour
duration: 7200, // 2 hours (50% progress)
lastUpdated: Date.now()
}
);
// Add a test series episode with 30% progress
await storageService.setWatchProgress(
'tt0944947', // Game of Thrones IMDB ID
'series',
{
currentTime: 1800, // 30 minutes
duration: 6000, // 100 minutes (30% progress)
lastUpdated: Date.now() - 86400000 // 1 day ago
},
'tt0944947:1:1' // Season 1, Episode 1
);
console.log('[HomeScreen] Test watch progress added successfully');
// Refresh the continue watching section
await refreshContinueWatching();
} catch (error) {
console.error('[HomeScreen] Error adding test watch progress:', error);
}
}, [refreshContinueWatching]);
// Debug function to clear all watch progress
const clearAllWatchProgress = useCallback(async () => {
console.log('[HomeScreen] Clearing all watch progress...');
try {
const allProgress = await storageService.getAllWatchProgress();
for (const key of Object.keys(allProgress)) {
const [type, id, episodeId] = key.split(':');
await storageService.removeWatchProgress(id, type, episodeId);
}
console.log('[HomeScreen] All watch progress cleared');
await refreshContinueWatching();
} catch (error) {
console.error('[HomeScreen] Error clearing watch progress:', error);
}
}, [refreshContinueWatching]);
return isLoading ? renderLoadingScreen : renderMainContent;
};

View file

@ -415,14 +415,11 @@ class StremioService {
});
}
logger.log(`Cinemeta catalog request URL: ${url}`);
const response = await this.retryRequest(async () => {
return await axios.get(url);
});
if (response.data && response.data.metas && Array.isArray(response.data.metas)) {
logger.log(`Cinemeta returned ${response.data.metas.length} items`);
return response.data.metas;
}
return [];
@ -453,14 +450,11 @@ class StremioService {
});
}
logger.log(`${manifest.name} catalog request URL: ${url}`);
const response = await this.retryRequest(async () => {
return await axios.get(url);
});
if (response.data && response.data.metas && Array.isArray(response.data.metas)) {
logger.log(`${manifest.name} returned ${response.data.metas.length} items`);
return response.data.metas;
}
return [];