diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 53d1ab40f..4b69e62bf 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -491,8 +491,8 @@ const ContinueWatchingSection = React.forwardRef((props, re logger.log(`[CW] Providers authed: trakt=${isTraktAuthed} simkl=${isSimklAuthed}`); - // Declare groupPromises outside the if block let groupPromises: Promise[] = []; + const allLocalItems: ContinueWatchingItem[] = []; // In Trakt mode, CW is sourced from Trakt only, but we still want to overlay local progress // when local is ahead (scrobble lag/offline playback). @@ -644,7 +644,6 @@ const ContinueWatchingSection = React.forwardRef((props, re } })(); - // Process each content group concurrently, merging results as they arrive groupPromises = Object.values(contentGroups).map(async (group) => { try { if (!isSupportedId(group.id)) return; @@ -801,7 +800,7 @@ const ContinueWatchingSection = React.forwardRef((props, re } as ContinueWatchingItem); } - if (batch.length > 0) await mergeBatchIntoState(batch); + if (batch.length > 0) allLocalItems.push(...batch); } catch (error) { // Continue processing other groups even if one fails } @@ -1196,7 +1195,7 @@ const ContinueWatchingSection = React.forwardRef((props, re // Always preferring local was wrong: if you watched on another device, // Trakt's paused_at is newer and should win for ordering purposes. const mergedLastUpdated = Math.max( - (mostRecentLocal.lastUpdated ?? 0), + (mostRecentLocal.lastUpdated ?? 0), (it.lastUpdated ?? 0) ); @@ -1573,6 +1572,37 @@ const ContinueWatchingSection = React.forwardRef((props, re // Wait for all groups and provider merges to settle, then finalize loading state await Promise.allSettled([...groupPromises, traktMergePromise, simklMergePromise]); + + if (allLocalItems.length > 0) { + const map = new Map(); + for (const it of allLocalItems) { + const key = `${it.type}:${it.id}`; + const existing = map.get(key); + if (!existing || shouldPreferCandidate(it, existing)) { + map.set(key, it); + } + } + + const sorted = Array.from(map.values()); + sorted.sort(compareCwItems); + + // Filter removed items + const filtered: ContinueWatchingItem[] = []; + for (const it of sorted) { + const key = it.type === 'series' && it.season && it.episode + ? `${it.type}:${it.id}:${it.season}:${it.episode}` + : `${it.type}:${it.id}`; + if (recentlyRemovedRef.current.has(key)) continue; + + const removeId = it.type === 'series' && it.season && it.episode + ? `${it.id}:${it.season}:${it.episode}` + : it.id; + const isRemoved = await storageService.isContinueWatchingRemoved(removeId, it.type); + if (!isRemoved) filtered.push(it); + } + + setContinueWatchingItems(filtered); + } } catch (error) { // Continue even if loading fails } finally { @@ -1943,271 +1973,271 @@ const ContinueWatchingSection = React.forwardRef((props, re // Memoized render function for poster-style continue watching items const renderPosterStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => { return ( - handleContentPress(item)} - onLongPress={() => handleLongPress(item)} - delayLongPress={800} - > - {/* Poster Image */} - - + handleContentPress(item)} + onLongPress={() => handleLongPress(item)} + delayLongPress={800} + > + {/* Poster Image */} + + - {/* Gradient overlay */} - + {/* Gradient overlay */} + - {/* Episode Info Overlay */} - {item.type === 'series' && item.season && item.episode && ( - - - S{item.season} E{item.episode} - - - )} - - {/* Up Next Badge */} - {item.type === 'series' && item.progress === 0 && ( - - {t('home.up_next_caps')} - - )} - - {/* Progress Bar */} - {item.progress > 0 && ( - - - + {/* Episode Info Overlay */} + {item.type === 'series' && item.season && item.episode && ( + + + S{item.season} E{item.episode} + - - )} + )} - {/* Delete Indicator Overlay */} - {deletingItemId === item.id && ( - - - - )} - + {/* Up Next Badge */} + {item.type === 'series' && item.progress === 0 && ( + + {t('home.up_next_caps')} + + )} - {/* Title below poster */} - - - {item.name} - - {item.progress > 0 && ( - - {Math.round(item.progress)}% + {/* Progress Bar */} + {item.progress > 0 && ( + + + + + + )} + + {/* Delete Indicator Overlay */} + {deletingItemId === item.id && ( + + + + )} + + + {/* Title below poster */} + + + {item.name} - )} - - + {item.progress > 0 && ( + + {Math.round(item.progress)}% + + )} + + ); }, [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedPosterWidth, computedPosterHeight, isTV, isLargeTablet, settings.posterBorderRadius]); // Memoized render function for wide-style continue watching items const renderWideStyleItem = useCallback(({ item }: { item: ContinueWatchingItem }) => { return ( - handleContentPress(item)} - onLongPress={() => handleLongPress(item)} - delayLongPress={800} - > - {/* Poster Image */} - - + handleContentPress(item)} + onLongPress={() => handleLongPress(item)} + delayLongPress={800} + > + {/* Poster Image */} + + - {/* Delete Indicator Overlay */} - {deletingItemId === item.id && ( - - - - )} - - - {/* Content Details */} - - {(() => { - const isUpNext = item.type === 'series' && item.progress === 0; - return ( - - - {item.name} - - {isUpNext && ( - - {t('home.up_next')} - - )} + {/* Delete Indicator Overlay */} + {deletingItemId === item.id && ( + + - ); - })()} + )} + - {/* Episode Info or Year */} - {(() => { - if (item.type === 'series' && item.season && item.episode) { + {/* Content Details */} + + {(() => { + const isUpNext = item.type === 'series' && item.progress === 0; return ( - + + + {item.name} + + {isUpNext && ( + + {t('home.up_next')} + + )} + + ); + })()} + + {/* Episode Info or Year */} + {(() => { + if (item.type === 'series' && item.season && item.episode) { + return ( + + + {t('home.season', { season: item.season })} + + {item.episodeTitle && ( + + {item.episodeTitle} + + )} + + ); + } else { + return ( - {t('home.season', { season: item.season })} + {item.year} • {item.type === 'movie' ? t('home.movie') : t('home.series')} - {item.episodeTitle && ( - - {item.episodeTitle} - - )} - - ); - } else { - return ( - 0 && ( + + - {item.year} • {item.type === 'movie' ? t('home.movie') : t('home.series')} + + + + {t('home.percent_watched', { percent: Math.round(item.progress) })} - ); - } - })()} - - {/* Progress Bar */} - {item.progress > 0 && ( - - - - - {t('home.percent_watched', { percent: Math.round(item.progress) })} - - - )} - - + )} + + ); }, [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet, settings.posterBorderRadius, t]);