Refactor watch progress management and enhance episode scrolling functionality

This update improves the watch progress management by incorporating last updated timestamps for episodes, allowing for more accurate tracking of the most recently watched content. The SeriesContent component now automatically selects the season based on the most recent watch progress, enhancing user experience. Additionally, the scrolling functionality to the most recently watched episode has been implemented, ensuring users can easily continue watching from where they left off. UI adjustments in the ContinueWatchingSection and HomeScreen have also been made for better layout consistency.
This commit is contained in:
tapframe 2025-06-20 01:58:48 +05:30
parent 6c2259f6e8
commit c0a63b3c53
5 changed files with 161 additions and 51 deletions

View file

@ -393,16 +393,16 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
<View style={styles.episodeRow}>
<Text style={[styles.episodeText, { color: currentTheme.colors.mediumEmphasis }]}>
Season {item.season}
</Text>
{item.episodeTitle && (
</Text>
{item.episodeTitle && (
<Text
style={[styles.episodeTitle, { color: currentTheme.colors.mediumEmphasis }]}
numberOfLines={1}
>
{item.episodeTitle}
</Text>
)}
</View>
{item.episodeTitle}
</Text>
)}
</View>
);
} else {
return (
@ -416,15 +416,15 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
{/* Progress Bar */}
<View style={styles.wideProgressContainer}>
<View style={styles.wideProgressTrack}>
<View
style={[
<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

View file

@ -40,16 +40,17 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
const { width } = useWindowDimensions();
const isTablet = width > 768;
const isDarkMode = useColorScheme() === 'dark';
const [episodeProgress, setEpisodeProgress] = useState<{ [key: string]: { currentTime: number; duration: number } }>({});
const [episodeProgress, setEpisodeProgress] = useState<{ [key: string]: { currentTime: number; duration: number; lastUpdated: number } }>({});
// Add ref for the season selector ScrollView
// Add refs for the scroll views
const seasonScrollViewRef = useRef<ScrollView | null>(null);
const episodeScrollViewRef = useRef<ScrollView | null>(null);
const loadEpisodesProgress = async () => {
if (!metadata?.id) return;
const allProgress = await storageService.getAllWatchProgress();
const progress: { [key: string]: { currentTime: number; duration: number } } = {};
const progress: { [key: string]: { currentTime: number; duration: number; lastUpdated: number } } = {};
episodes.forEach(episode => {
const episodeId = episode.stremioId || `${metadata.id}:${episode.season_number}:${episode.episode_number}`;
@ -57,7 +58,8 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
if (allProgress[key]) {
progress[episodeId] = {
currentTime: allProgress[key].currentTime,
duration: allProgress[key].duration
duration: allProgress[key].duration,
lastUpdated: allProgress[key].lastUpdated
};
}
});
@ -65,6 +67,67 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
setEpisodeProgress(progress);
};
// Function to find and scroll to the most recently watched episode
const scrollToMostRecentEpisode = () => {
if (!metadata?.id || !episodeScrollViewRef.current || settings.episodeLayoutStyle !== 'horizontal') {
console.log('[SeriesContent] Scroll conditions not met:', {
hasMetadataId: !!metadata?.id,
hasScrollRef: !!episodeScrollViewRef.current,
isHorizontal: settings.episodeLayoutStyle === 'horizontal'
});
return;
}
const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || [];
if (currentSeasonEpisodes.length === 0) {
console.log('[SeriesContent] No episodes in current season:', selectedSeason);
return;
}
// Find the most recently watched episode in the current season
let mostRecentEpisodeIndex = -1;
let mostRecentTimestamp = 0;
let mostRecentEpisodeName = '';
currentSeasonEpisodes.forEach((episode, index) => {
const episodeId = episode.stremioId || `${metadata.id}:${episode.season_number}:${episode.episode_number}`;
const progress = episodeProgress[episodeId];
if (progress && progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) {
mostRecentTimestamp = progress.lastUpdated;
mostRecentEpisodeIndex = index;
mostRecentEpisodeName = episode.name;
}
});
console.log('[SeriesContent] Episode scroll analysis:', {
totalEpisodes: currentSeasonEpisodes.length,
mostRecentIndex: mostRecentEpisodeIndex,
mostRecentEpisode: mostRecentEpisodeName,
selectedSeason
});
// Scroll to the most recently watched episode if found
if (mostRecentEpisodeIndex >= 0) {
const cardWidth = isTablet ? width * 0.4 + 16 : width * 0.85 + 16;
const scrollPosition = mostRecentEpisodeIndex * cardWidth;
console.log('[SeriesContent] Scrolling to episode:', {
index: mostRecentEpisodeIndex,
cardWidth,
scrollPosition,
episodeName: mostRecentEpisodeName
});
setTimeout(() => {
episodeScrollViewRef.current?.scrollTo({
x: scrollPosition,
animated: true
});
}, 500); // Delay to ensure the season has loaded
}
};
// Initial load of watch progress
useEffect(() => {
loadEpisodesProgress();
@ -96,6 +159,13 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
}
}, [selectedSeason, groupedEpisodes]);
// Add effect to scroll to most recently watched episode when season changes or progress loads
useEffect(() => {
if (Object.keys(episodeProgress).length > 0 && selectedSeason) {
scrollToMostRecentEpisode();
}
}, [selectedSeason, episodeProgress, settings.episodeLayoutStyle, groupedEpisodes]);
if (loadingSeasons) {
return (
<View style={styles.centeredContainer}>
@ -480,6 +550,7 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
{settings.episodeLayoutStyle === 'horizontal' ? (
// Horizontal Layout (Netflix-style)
<ScrollView
ref={episodeScrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
style={styles.episodeList}

View file

@ -11,6 +11,7 @@ import { logger } from '../utils/logger';
import { usePersistentSeasons } from './usePersistentSeasons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Stream } from '../types/metadata';
import { storageService } from '../services/storageService';
// Constants for timeouts and retries
const API_TIMEOUT = 10000; // 10 seconds
@ -589,14 +590,61 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
// Get the first available season as fallback
const firstSeason = Math.min(...Object.keys(allEpisodes).map(Number));
// Get saved season from persistence, fallback to first season if not found
const persistedSeason = getSeason(id, firstSeason);
// Check for watch progress to auto-select season
let selectedSeasonNumber = firstSeason;
// Set the selected season from persistence
setSelectedSeason(persistedSeason);
try {
// Check watch progress for auto-season selection
const allProgress = await storageService.getAllWatchProgress();
// Find the most recently watched episode for this series
let mostRecentEpisodeId = '';
let mostRecentTimestamp = 0;
Object.entries(allProgress).forEach(([key, progress]) => {
if (key.includes(`series:${id}:`)) {
const episodeId = key.split(`series:${id}:`)[1];
if (progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) {
mostRecentTimestamp = progress.lastUpdated;
mostRecentEpisodeId = episodeId;
}
}
});
if (mostRecentEpisodeId) {
// Parse season number from episode ID
const parts = mostRecentEpisodeId.split(':');
if (parts.length === 3) {
const watchProgressSeason = parseInt(parts[1], 10);
if (transformedEpisodes[watchProgressSeason]) {
selectedSeasonNumber = watchProgressSeason;
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for ${mostRecentEpisodeId}`);
}
} else {
// Try to find episode by stremioId to get season
const allEpisodesList = Object.values(transformedEpisodes).flat();
const episode = allEpisodesList.find(ep => ep.stremioId === mostRecentEpisodeId);
if (episode) {
selectedSeasonNumber = episode.season_number;
logger.log(`[useMetadata] Auto-selected season ${selectedSeasonNumber} based on most recent watch progress for episode with stremioId ${mostRecentEpisodeId}`);
}
}
} else {
// No watch progress found, use persistent storage as fallback
selectedSeasonNumber = getSeason(id, firstSeason);
logger.log(`[useMetadata] No watch progress found, using persistent season ${selectedSeasonNumber}`);
}
} catch (error) {
logger.error('[useMetadata] Error checking watch progress for season selection:', error);
// Fall back to persistent storage
selectedSeasonNumber = getSeason(id, firstSeason);
}
// Set the selected season
setSelectedSeason(selectedSeasonNumber);
// Set episodes for the selected season
setEpisodes(transformedEpisodes[persistedSeason] || []);
setEpisodes(transformedEpisodes[selectedSeasonNumber] || []);
}
} catch (error) {
console.error('Failed to load episodes:', error);

View file

@ -103,36 +103,27 @@ export const useWatchProgress = (
setWatchProgress(null);
}
} else {
// Find the first unfinished episode
const unfinishedEpisode = episodes.find(ep => {
const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`;
const progress = seriesProgresses.find(p => p.episodeId === epId);
if (!progress) return true;
const percent = (progress.progress.currentTime / progress.progress.duration) * 100;
return percent < 95;
});
if (unfinishedEpisode) {
const epId = unfinishedEpisode.stremioId ||
`${id}:${unfinishedEpisode.season_number}:${unfinishedEpisode.episode_number}`;
const progress = await storageService.getWatchProgress(id, type, epId);
if (progress) {
setWatchProgress({
...progress,
episodeId: epId,
traktSynced: progress.traktSynced,
traktProgress: progress.traktProgress
});
} else {
setWatchProgress({
currentTime: 0,
duration: 0,
lastUpdated: Date.now(),
episodeId: epId,
traktSynced: false
});
}
// FIXED: Find the most recently watched episode instead of first unfinished
// Sort by lastUpdated timestamp (most recent first)
const sortedProgresses = seriesProgresses.sort((a, b) =>
b.progress.lastUpdated - a.progress.lastUpdated
);
if (sortedProgresses.length > 0) {
// Use the most recently watched episode
const mostRecentProgress = sortedProgresses[0];
const progress = mostRecentProgress.progress;
logger.log(`[useWatchProgress] Using most recent progress for ${mostRecentProgress.episodeId}, updated at ${new Date(progress.lastUpdated).toLocaleString()}`);
setWatchProgress({
...progress,
episodeId: mostRecentProgress.episodeId,
traktSynced: progress.traktSynced,
traktProgress: progress.traktProgress
});
} else {
// No watched episodes found
setWatchProgress(null);
}
}

View file

@ -533,9 +533,9 @@ const HomeScreen = () => {
console.log('[HomeScreen] Refreshing continue watching...');
if (continueWatchingRef.current) {
try {
const hasContent = await continueWatchingRef.current.refresh();
const hasContent = await continueWatchingRef.current.refresh();
console.log(`[HomeScreen] Continue watching has content: ${hasContent}`);
setHasContinueWatching(hasContent);
setHasContinueWatching(hasContent);
// Debug: Let's check what's in storage
const allProgress = await storageService.getAllWatchProgress();
@ -667,7 +667,7 @@ const HomeScreen = () => {
</TouchableOpacity>
</View>
<ContinueWatchingSection ref={continueWatchingRef} />
<ContinueWatchingSection ref={continueWatchingRef} />
{catalogs.length > 0 ? (
catalogs.map((catalog, index) => (