feat(series): surface uncategorized episodes as synthetic season

Episodes missing season/identity fields from Jellyfin previously crashed
the converter due to non-null assertions. Introduce an Uncategorized
sentinel season appended to a series via Series.allSeasons so these
episodes are selectable in the season tabs on both phone and TV.

- Add UNCATEGORIZED_SEASON_ID/SERIES_ID/LABEL constants in core-model
- Make BaseItemDtoConverter null-safe and partition uncategorized episodes
- InMemoryLocalMediaRepository collects uncategorized episodes per series
- OfflineRoomMediaLocalDataSource persists and reconstructs the sentinel
- SeriesScreen and TvSeriesScreen use allSeasons for tab rendering
This commit is contained in:
2026-07-22 18:36:02 +00:00
parent e33a92271a
commit d8de27c972
6 changed files with 164 additions and 43 deletions

View File

@@ -78,10 +78,10 @@ internal fun TvSeriesScreenContent(
var selectedSeasonId by remember(series.id, focusedSeasonId) {
mutableStateOf(defaultSeason?.id)
}
val selectedSeason = series.seasons.firstOrNull { it.id == selectedSeasonId } ?: defaultSeason
val selectedSeason = series.allSeasons.firstOrNull { it.id == selectedSeasonId } ?: defaultSeason
val initialFocusSeasonId = defaultSeason?.id
val initialFocusSeason = initialFocusSeasonId?.let { seasonId ->
series.seasons.firstOrNull { it.id == seasonId }
series.allSeasons.firstOrNull { it.id == seasonId }
} ?: defaultSeason
val initialFocusedEpisodeId = initialFocusSeason?.focusTargetEpisodeId(focusedEpisodeId)
val seasonTabFocusRequester = remember { FocusRequester() }
@@ -120,7 +120,7 @@ internal fun TvSeriesScreenContent(
Spacer(modifier = Modifier.height(16.dp))
if (selectedSeason != null) {
TvSeasonTabs(
seasons = series.seasons,
seasons = series.allSeasons,
selectedSeason = selectedSeason,
selectedItemFocusRequester = seasonTabFocusRequester,
firstItemTestTag = SeriesFirstSeasonTabTag,
@@ -156,10 +156,10 @@ internal fun TvSeriesScreenContent(
private fun Series.defaultSeason(focusedSeasonId: UUID?): Season? {
if (focusedSeasonId != null) {
seasons.firstOrNull { it.id == focusedSeasonId }?.let { return it }
allSeasons.firstOrNull { it.id == focusedSeasonId }?.let { return it }
}
return seasons.firstOrNull { it.unwatchedEpisodeCount > 0 } ?: seasons.firstOrNull()
return allSeasons.firstOrNull { it.unwatchedEpisodeCount > 0 } ?: allSeasons.firstOrNull()
}
private fun Season.nextUpEpisode(): Episode? {