continue watching with episode history limits and UI updates

This commit is contained in:
tapframe 2026-02-12 20:05:21 +05:30
parent 07ae0d889d
commit a1bd550bbc
4 changed files with 84 additions and 3 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@ android-tv-samples
JustPlayer
TODO.local.md
local.dev.properties
trakt-docss

View file

@ -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<String, WatchProgress>()
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<String, WatchProgress>()
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
}
}

View file

@ -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

View file

@ -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) {