diff --git a/.gitignore b/.gitignore index 426b315e..44ef94a3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ android-tv-samples JustPlayer TODO.local.md local.dev.properties +trakt-docss \ No newline at end of file diff --git a/app/src/main/java/com/nuvio/tv/data/local/WatchProgressPreferences.kt b/app/src/main/java/com/nuvio/tv/data/local/WatchProgressPreferences.kt index be743f49..949496bd 100644 --- a/app/src/main/java/com/nuvio/tv/data/local/WatchProgressPreferences.kt +++ b/app/src/main/java/com/nuvio/tv/data/local/WatchProgressPreferences.kt @@ -28,6 +28,10 @@ class WatchProgressPreferences @Inject constructor( // Maximum items to keep in continue watching private val maxItems = 50 + + private val maxEpisodesPerContent = 25 + + private val maxStoredEntries = 300 /** * Get all watch progress items, sorted by last watched (most recent first) @@ -199,8 +203,47 @@ class WatchProgressPreferences @Inject constructor( .sortedByDescending { it.value } .take(maxItems) .map { it.key } - .toSet() + val keepContentIdSet = keepContentIds.toSet() - return map.filterValues { it.contentId in keepContentIds } + val filteredByContent = map.filterValues { it.contentId in keepContentIdSet } + val boundedByContent = mutableMapOf() + + keepContentIds.forEach { contentId -> + val entriesForContent = filteredByContent.filterValues { it.contentId == contentId } + + // Keep canonical content-level record when present. + entriesForContent[contentId]?.let { boundedByContent[contentId] = it } + + val recentEpisodeEntries = entriesForContent + .filterKeys { it != contentId } + .entries + .sortedByDescending { it.value.lastWatched } + .take(maxEpisodesPerContent) + + recentEpisodeEntries.forEach { (key, value) -> + boundedByContent[key] = value + } + } + + if (boundedByContent.size <= maxStoredEntries) return boundedByContent + + val pinnedContentKeys = keepContentIds.filter { boundedByContent.containsKey(it) }.toSet() + val remainingSlots = (maxStoredEntries - pinnedContentKeys.size).coerceAtLeast(0) + + val limited = mutableMapOf() + pinnedContentKeys.forEach { key -> + boundedByContent[key]?.let { limited[key] = it } + } + + boundedByContent.entries + .asSequence() + .filter { (key, _) -> key !in pinnedContentKeys } + .sortedByDescending { (_, value) -> value.lastWatched } + .take(remainingSlots) + .forEach { (key, value) -> + limited[key] = value + } + + return limited } } diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/home/GridHomeContent.kt b/app/src/main/java/com/nuvio/tv/ui/screens/home/GridHomeContent.kt index cf9a98e3..bec21fe8 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/home/GridHomeContent.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/home/GridHomeContent.kt @@ -306,6 +306,42 @@ fun GridHomeContent( } } } + + if (!continueWatchingInserted && uiState.continueWatchingItems.isNotEmpty()) { + item( + key = "continue_watching_fallback", + span = { TvGridItemSpan(maxLineSpan) }, + contentType = "continue_watching" + ) { + GridContinueWatchingSection( + items = uiState.continueWatchingItems, + focusedItemIndex = if (shouldRequestInitialFocus && !hasHero) 0 else -1, + onItemClick = { item -> + onContinueWatchingClick(item) + }, + onDetailsClick = { item -> + onNavigateToDetail( + when (item) { + is ContinueWatchingItem.InProgress -> item.progress.contentId + is ContinueWatchingItem.NextUp -> item.info.contentId + }, + when (item) { + is ContinueWatchingItem.InProgress -> item.progress.contentType + is ContinueWatchingItem.NextUp -> item.info.contentType + }, + "" + ) + }, + onRemoveItem = { item -> + val contentId = when (item) { + is ContinueWatchingItem.InProgress -> item.progress.contentId + is ContinueWatchingItem.NextUp -> item.info.contentId + } + onRemoveContinueWatching(contentId) + } + ) + } + } } // Sticky header overlay diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt index 809477d7..25c75d9a 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/home/HomeScreen.kt @@ -52,7 +52,8 @@ fun HomeScreen( val gridFocusState by viewModel.gridFocusState.collectAsState() val hasHeroContent = uiState.heroSectionEnabled && uiState.heroItems.isNotEmpty() val hasCatalogContent = uiState.catalogRows.any { it.items.isNotEmpty() } - val hasPrimaryHomeContent = hasHeroContent || hasCatalogContent + val hasContinueWatchingContent = uiState.continueWatchingItems.isNotEmpty() + val hasPrimaryHomeContent = hasHeroContent || hasCatalogContent || hasContinueWatchingContent var hasEnteredHomeContent: Boolean by rememberSaveable { mutableStateOf(false) } LaunchedEffect(hasPrimaryHomeContent) {