diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx
index b74cdc86..7f36a029 100644
--- a/src/components/metadata/HeroSection.tsx
+++ b/src/components/metadata/HeroSection.tsx
@@ -77,7 +77,9 @@ const ActionButtons = React.memo(({
id,
navigation,
playButtonText,
- animatedStyle
+ animatedStyle,
+ isWatched,
+ watchProgress
}: {
handleShowStreams: () => void;
toggleLibrary: () => void;
@@ -87,6 +89,8 @@ const ActionButtons = React.memo(({
navigation: any;
playButtonText: string;
animatedStyle: any;
+ isWatched: boolean;
+ watchProgress: any;
}) => {
const { currentTheme } = useTheme();
@@ -122,19 +126,48 @@ const ActionButtons = React.memo(({
}
}, [id, navigation]);
+ // Determine play button style and text based on watched status
+ const playButtonStyle = useMemo(() => {
+ if (isWatched) {
+ return [styles.actionButton, styles.playButton, styles.watchedPlayButton];
+ }
+ return [styles.actionButton, styles.playButton];
+ }, [isWatched]);
+
+ const playButtonTextStyle = useMemo(() => {
+ if (isWatched) {
+ return [styles.playButtonText, styles.watchedPlayButtonText];
+ }
+ return styles.playButtonText;
+ }, [isWatched]);
+
+ const finalPlayButtonText = useMemo(() => {
+ if (isWatched) {
+ return 'Watch Again';
+ }
+ return playButtonText;
+ }, [isWatched, playButtonText]);
+
return (
- {playButtonText}
+ {finalPlayButtonText}
+
+ {/* Subtle watched indicator in play button */}
+ {isWatched && (
+
+
+
+ )}
{ seasonNumber: string; episodeNumber: string; episodeName: string } | null;
animatedStyle: any;
+ isWatched: boolean;
}) => {
const { currentTheme } = useTheme();
- const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
+ const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext();
+
+ // Handle manual Trakt sync
+ const handleTraktSync = useMemo(() => async () => {
+ if (isTraktAuthenticated && forceSyncTraktProgress) {
+ logger.log('[HeroSection] Manual Trakt sync requested');
+ try {
+ const success = await forceSyncTraktProgress();
+ logger.log(`[HeroSection] Manual Trakt sync ${success ? 'successful' : 'failed'}`);
+ } catch (error) {
+ logger.error('[HeroSection] Manual Trakt sync error:', error);
+ }
+ }
+ }, [isTraktAuthenticated, forceSyncTraktProgress]);
// Memoized progress calculation with Trakt integration
const progressData = useMemo(() => {
+ // If content is fully watched, show watched status instead of progress
+ if (isWatched) {
+ let episodeInfo = '';
+ if (type === 'series' && watchProgress?.episodeId) {
+ const details = getEpisodeDetails(watchProgress.episodeId);
+ if (details) {
+ episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`;
+ }
+ }
+
+ const watchedDate = watchProgress?.lastUpdated
+ ? new Date(watchProgress.lastUpdated).toLocaleDateString()
+ : new Date().toLocaleDateString();
+
+ // Determine if watched via Trakt or local
+ const watchedViaTrakt = isTraktAuthenticated &&
+ watchProgress?.traktProgress !== undefined &&
+ watchProgress.traktProgress >= 95;
+
+ return {
+ progressPercent: 100,
+ formattedTime: watchedDate,
+ episodeInfo,
+ displayText: watchedViaTrakt ? 'Watched on Trakt' : 'Watched',
+ syncStatus: isTraktAuthenticated && watchProgress?.traktSynced ? '' : '', // Clean look for watched
+ isTraktSynced: watchProgress?.traktSynced && isTraktAuthenticated,
+ isWatched: true
+ };
+ }
+
if (!watchProgress || watchProgress.duration === 0) return null;
- const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
+ // Determine which progress to show - prioritize Trakt if available and authenticated
+ let progressPercent;
+ let isUsingTraktProgress = false;
+
+ if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) {
+ progressPercent = watchProgress.traktProgress;
+ isUsingTraktProgress = true;
+ } else {
+ progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
+ }
const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString();
let episodeInfo = '';
@@ -242,7 +329,12 @@ const WatchProgressDisplay = React.memo(({
// Show Trakt sync status if user is authenticated
if (isTraktAuthenticated) {
- if (watchProgress.traktSynced) {
+ if (isUsingTraktProgress) {
+ syncStatus = ' • Using Trakt progress';
+ if (watchProgress.traktSynced) {
+ syncStatus = ' • Synced with Trakt';
+ }
+ } else if (watchProgress.traktSynced) {
syncStatus = ' • Synced with Trakt';
// If we have specific Trakt progress that differs from local, mention it
if (watchProgress.traktProgress !== undefined &&
@@ -260,9 +352,10 @@ const WatchProgressDisplay = React.memo(({
episodeInfo,
displayText,
syncStatus,
- isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated
+ isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated,
+ isWatched: false
};
- }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated]);
+ }, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated, isWatched]);
if (!progressData) return null;
@@ -274,14 +367,26 @@ const WatchProgressDisplay = React.memo(({
styles.watchProgressFill,
{
width: `${progressData.progressPercent}%`,
- backgroundColor: progressData.isTraktSynced
- ? '#E50914' // Netflix red for Trakt synced content
- : currentTheme.colors.primary
+ backgroundColor: progressData.isWatched
+ ? '#666' // Subtle gray for completed
+ : progressData.isTraktSynced
+ ? '#E50914' // Netflix red for Trakt synced content
+ : currentTheme.colors.primary
}
]}
/>
- {/* Trakt sync indicator */}
- {progressData.isTraktSynced && (
+ {/* Subtle watched indicator */}
+ {progressData.isWatched && (
+
+
+
+ )}
+ {/* Trakt sync indicator for non-watched content */}
+ {progressData.isTraktSynced && !progressData.isWatched && (
)}
-
- {progressData.displayText}{progressData.episodeInfo} • Last watched on {progressData.formattedTime}
- {progressData.syncStatus}
-
+
+
+ {progressData.displayText}{progressData.episodeInfo} • Last watched on {progressData.formattedTime}
+ {progressData.syncStatus}
+
+
+ {/* Manual Trakt sync button */}
+ {isTraktAuthenticated && forceSyncTraktProgress && (
+
+
+
+ )}
+
);
});
@@ -452,6 +577,25 @@ const HeroSection: React.FC = ({
// Memoized play button text
const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]);
+ // Calculate if content is watched (>=95% progress) - check both local and Trakt progress
+ const isWatched = useMemo(() => {
+ if (!watchProgress) return false;
+
+ // Check Trakt progress first if available and user is authenticated
+ if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) {
+ const traktWatched = watchProgress.traktProgress >= 95;
+ logger.log(`[HeroSection] Trakt authenticated: ${isTraktAuthenticated}, Trakt progress: ${watchProgress.traktProgress}%, Watched: ${traktWatched}`);
+ return traktWatched;
+ }
+
+ // Fall back to local progress
+ if (watchProgress.duration === 0) return false;
+ const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
+ const localWatched = progressPercent >= 95;
+ logger.log(`[HeroSection] Local progress: ${progressPercent.toFixed(1)}%, Watched: ${localWatched}`);
+ return localWatched;
+ }, [watchProgress, isTraktAuthenticated]);
+
return (
{/* Optimized Background */}
@@ -521,6 +665,7 @@ const HeroSection: React.FC = ({
type={type}
getEpisodeDetails={getEpisodeDetails}
animatedStyle={watchProgressAnimatedStyle}
+ isWatched={isWatched}
/>
{/* Optimized Genres */}
@@ -540,6 +685,8 @@ const HeroSection: React.FC = ({
navigation={navigation}
playButtonText={playButtonText}
animatedStyle={buttonsAnimatedStyle}
+ isWatched={isWatched}
+ watchProgress={watchProgress}
/>
@@ -623,6 +770,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
width: '100%',
+ position: 'relative',
},
actionButton: {
flexDirection: 'row',
@@ -697,11 +845,32 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
+ watchedProgressIndicator: {
+ position: 'absolute',
+ right: 2,
+ top: -1,
+ bottom: -1,
+ width: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ watchProgressTextContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ },
watchProgressText: {
fontSize: 11,
textAlign: 'center',
opacity: 0.85,
- letterSpacing: 0.1
+ letterSpacing: 0.1,
+ flex: 1,
+ },
+ traktSyncButton: {
+ padding: 4,
+ borderRadius: 12,
+ backgroundColor: 'rgba(255,255,255,0.1)',
},
blurBackground: {
position: 'absolute',
@@ -737,6 +906,33 @@ const styles = StyleSheet.create({
borderRadius: 25,
backgroundColor: 'rgba(255,255,255,0.15)',
},
+ watchedIndicator: {
+ position: 'absolute',
+ top: 4,
+ right: 4,
+ backgroundColor: 'rgba(0,0,0,0.6)',
+ borderRadius: 8,
+ width: 16,
+ height: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ watchedPlayButton: {
+ backgroundColor: '#1e1e1e',
+ borderWidth: 1,
+ borderColor: 'rgba(255,255,255,0.3)',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ elevation: 4,
+ },
+ watchedPlayButtonText: {
+ color: '#fff',
+ fontWeight: '700',
+ marginLeft: 6,
+ fontSize: 15,
+ },
});
export default React.memo(HeroSection);
\ No newline at end of file
diff --git a/src/hooks/useTraktIntegration.ts b/src/hooks/useTraktIntegration.ts
index 7b4e71b2..27c414b5 100644
--- a/src/hooks/useTraktIntegration.ts
+++ b/src/hooks/useTraktIntegration.ts
@@ -281,15 +281,16 @@ export function useTraktIntegration() {
}
try {
- logger.log('[useTraktIntegration] Fetching Trakt playback progress...');
- const traktProgress = await getTraktPlaybackProgress();
- logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items`);
+ // Fetch both playback progress and recently watched movies
+ logger.log('[useTraktIntegration] Fetching Trakt playback progress and watched movies...');
+ const [traktProgress, watchedMovies] = await Promise.all([
+ getTraktPlaybackProgress(),
+ traktService.getWatchedMovies()
+ ]);
- if (traktProgress.length === 0) {
- logger.log('[useTraktIntegration] No Trakt progress found - user may not have any content in progress');
- return true; // Not an error, just no data
- }
+ logger.log(`[useTraktIntegration] Retrieved ${traktProgress.length} Trakt progress items, ${watchedMovies.length} watched movies`);
+ // Process playback progress (in-progress items)
for (const item of traktProgress) {
try {
let id: string;
@@ -299,14 +300,14 @@ export function useTraktIntegration() {
if (item.type === 'movie' && item.movie) {
id = item.movie.ids.imdb;
type = 'movie';
- logger.log(`[useTraktIntegration] Processing Trakt movie: ${item.movie.title} (${id}) - ${item.progress}%`);
+ logger.log(`[useTraktIntegration] Processing Trakt movie progress: ${item.movie.title} (${id}) - ${item.progress}%`);
} else if (item.type === 'episode' && item.show && item.episode) {
id = item.show.ids.imdb;
type = 'series';
episodeId = `${id}:${item.episode.season}:${item.episode.number}`;
- logger.log(`[useTraktIntegration] Processing Trakt episode: ${item.show.title} S${item.episode.season}E${item.episode.number} (${id}) - ${item.progress}%`);
+ logger.log(`[useTraktIntegration] Processing Trakt episode progress: ${item.show.title} S${item.episode.season}E${item.episode.number} (${id}) - ${item.progress}%`);
} else {
- logger.warn(`[useTraktIntegration] Skipping invalid Trakt item:`, item);
+ logger.warn(`[useTraktIntegration] Skipping invalid Trakt progress item:`, item);
continue;
}
@@ -323,7 +324,27 @@ export function useTraktIntegration() {
}
}
- logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} Trakt progress entries`);
+ // Process watched movies (100% completed)
+ for (const movie of watchedMovies) {
+ try {
+ if (movie.movie?.ids?.imdb) {
+ const id = movie.movie.ids.imdb;
+ const watchedAt = movie.last_watched_at;
+ logger.log(`[useTraktIntegration] Processing watched movie: ${movie.movie.title} (${id}) - 100% watched on ${watchedAt}`);
+
+ await storageService.mergeWithTraktProgress(
+ id,
+ 'movie',
+ 100, // 100% progress for watched items
+ watchedAt
+ );
+ }
+ } catch (error) {
+ logger.error('[useTraktIntegration] Error merging watched movie:', error);
+ }
+ }
+
+ logger.log(`[useTraktIntegration] Successfully merged ${traktProgress.length} progress items + ${watchedMovies.length} watched movies`);
return true;
} catch (error) {
logger.error('[useTraktIntegration] Error fetching and merging Trakt progress:', error);
@@ -362,6 +383,24 @@ export function useTraktIntegration() {
}
}, [isAuthenticated, fetchAndMergeTraktProgress]);
+ // Periodic sync - check for updates every 2 minutes when authenticated
+ useEffect(() => {
+ if (!isAuthenticated) return;
+
+ const intervalId = setInterval(() => {
+ logger.log('[useTraktIntegration] Periodic Trakt sync check');
+ fetchAndMergeTraktProgress().then((success) => {
+ if (success) {
+ logger.log('[useTraktIntegration] Periodic sync completed successfully');
+ }
+ }).catch(error => {
+ logger.error('[useTraktIntegration] Periodic sync failed:', error);
+ });
+ }, 2 * 60 * 1000); // 2 minutes
+
+ return () => clearInterval(intervalId);
+ }, [isAuthenticated, fetchAndMergeTraktProgress]);
+
// Trigger sync when auth status is manually refreshed (for login scenarios)
useEffect(() => {
if (isAuthenticated) {
diff --git a/src/hooks/useWatchProgress.ts b/src/hooks/useWatchProgress.ts
index 0dcc9df0..b17e4345 100644
--- a/src/hooks/useWatchProgress.ts
+++ b/src/hooks/useWatchProgress.ts
@@ -91,62 +91,8 @@ export const useWatchProgress = (
if (episodeId) {
const progress = await storageService.getWatchProgress(id, type, episodeId);
if (progress) {
- const progressPercent = (progress.currentTime / progress.duration) * 100;
-
- // If current episode is finished (≥95%), try to find next unwatched episode
- if (progressPercent >= 95) {
- const currentEpNum = getEpisodeNumber(episodeId);
- if (currentEpNum && episodes.length > 0) {
- // Find the next episode
- const nextEpisode = episodes.find(ep => {
- // First check in same season
- if (ep.season_number === currentEpNum.season && ep.episode_number > currentEpNum.episode) {
- const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`;
- const epProgress = seriesProgresses.find(p => p.episodeId === epId);
- if (!epProgress) return true;
- const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100;
- return percent < 95;
- }
- // Then check next seasons
- if (ep.season_number > currentEpNum.season) {
- const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`;
- const epProgress = seriesProgresses.find(p => p.episodeId === epId);
- if (!epProgress) return true;
- const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100;
- return percent < 95;
- }
- return false;
- });
-
- if (nextEpisode) {
- const nextEpisodeId = nextEpisode.stremioId ||
- `${id}:${nextEpisode.season_number}:${nextEpisode.episode_number}`;
- const nextProgress = await storageService.getWatchProgress(id, type, nextEpisodeId);
- if (nextProgress) {
- setWatchProgress({
- ...nextProgress,
- episodeId: nextEpisodeId,
- traktSynced: nextProgress.traktSynced,
- traktProgress: nextProgress.traktProgress
- });
- } else {
- setWatchProgress({
- currentTime: 0,
- duration: 0,
- lastUpdated: Date.now(),
- episodeId: nextEpisodeId,
- traktSynced: false
- });
- }
- return;
- }
- }
- // If no next episode found or current episode is finished, show no progress
- setWatchProgress(null);
- return;
- }
-
- // If current episode is not finished, show its progress
+ // Always show the current episode progress when viewing it specifically
+ // This allows HeroSection to properly display watched state
setWatchProgress({
...progress,
episodeId,
@@ -194,17 +140,14 @@ export const useWatchProgress = (
// For movies
const progress = await storageService.getWatchProgress(id, type, episodeId);
if (progress && progress.currentTime > 0) {
- const progressPercent = (progress.currentTime / progress.duration) * 100;
- if (progressPercent >= 95) {
- setWatchProgress(null);
- } else {
- setWatchProgress({
- ...progress,
- episodeId,
- traktSynced: progress.traktSynced,
- traktProgress: progress.traktProgress
- });
- }
+ // Always show progress data, even if watched (≥95%)
+ // The HeroSection will handle the "watched" state display
+ setWatchProgress({
+ ...progress,
+ episodeId,
+ traktSynced: progress.traktSynced,
+ traktProgress: progress.traktProgress
+ });
} else {
setWatchProgress(null);
}