Show season extras row on series overview (#1286)

## Description
Adds a row for season level extras on the series overview page below the
people rows. The extras will update when switching between season tabs.

Like elsewhere in the app, an single extra of a type is show inline and
multiple extras of the same type open a grid when clicked.

### Related issues
Closes #504

### Testing
Emulator

## Screenshots
<img width="1280" height="720" alt="season_extras Large"
src="https://github.com/user-attachments/assets/bc62bef4-66c8-4412-9eca-ec0105d0d0fa"
/>


## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-04-21 17:34:38 -04:00 committed by GitHub
parent 8f98526558
commit c9cab7a5c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 41 additions and 3 deletions

View file

@ -98,13 +98,15 @@ fun SeriesOverview(
val episodeRowFocusRequester = remember { FocusRequester() }
val castCrewRowFocusRequester = remember { FocusRequester() }
val guestStarRowFocusRequester = remember { FocusRequester() }
val extrasRowFocusRequester = remember { FocusRequester() }
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val series by viewModel.item.observeAsState(null)
val seasons by viewModel.seasons.observeAsState(listOf())
val seasons by viewModel.seasons.observeAsState(emptyList())
val episodes by viewModel.episodes.observeAsState(EpisodeList.Loading)
val peopleInEpisode by viewModel.peopleInEpisode.map { it.people }.observeAsState(listOf())
val seasonExtras by viewModel.extras.observeAsState(emptyList())
val peopleInEpisode by viewModel.peopleInEpisode.map { it.people }.observeAsState(emptyList())
val episodeList = (episodes as? EpisodeList.Success)?.episodes
val position by viewModel.position.collectAsState(SeriesOverviewPosition(0, 0))
@ -164,6 +166,7 @@ fun SeriesOverview(
EPISODE_ROW -> episodeRowFocusRequester
CAST_AND_CREW_ROW -> castCrewRowFocusRequester
GUEST_STAR_ROW -> guestStarRowFocusRequester
EXTRAS_ROW -> extrasRowFocusRequester
else -> episodeRowFocusRequester
},
"series_overview",
@ -280,11 +283,13 @@ fun SeriesOverview(
episodes = episodes,
chosenStreams = chosenStreams,
peopleInEpisode = peopleInEpisode,
seasonExtras = seasonExtras,
position = position,
firstItemFocusRequester = firstItemFocusRequester,
episodeRowFocusRequester = episodeRowFocusRequester,
castCrewRowFocusRequester = castCrewRowFocusRequester,
guestStarRowFocusRequester = guestStarRowFocusRequester,
extrasRowFocusRequester = extrasRowFocusRequester,
onChangeSeason = { index ->
if (index != position.seasonTabIndex) {
seasons.getOrNull(index)?.let { season ->
@ -364,6 +369,10 @@ fun SeriesOverview(
),
)
},
onClickExtra = { _, extra ->
rowFocused = EXTRAS_ROW
viewModel.navigateTo(extra.destination)
},
canDelete = { viewModel.canDelete(it, preferences.appPreferences) },
deleteOnClick = { showDeleteDialog = it },
modifier = modifier,
@ -435,3 +444,4 @@ fun SeriesOverview(
private const val EPISODE_ROW = 0
private const val CAST_AND_CREW_ROW = EPISODE_ROW + 1
private const val GUEST_STAR_ROW = CAST_AND_CREW_ROW + 1
private const val EXTRAS_ROW = GUEST_STAR_ROW + 1

View file

@ -34,7 +34,6 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
@ -47,11 +46,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.ExtrasItem
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.cards.ExtrasRow
import com.github.damontecres.wholphin.ui.cards.PersonRow
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.HeaderUtils
@ -74,6 +75,7 @@ fun SeriesOverviewContent(
series: BaseItem,
seasons: List<BaseItem?>,
episodes: EpisodeList,
seasonExtras: List<ExtrasItem>,
chosenStreams: ChosenStreams?,
peopleInEpisode: List<Person>,
position: SeriesOverviewPosition,
@ -81,6 +83,7 @@ fun SeriesOverviewContent(
episodeRowFocusRequester: FocusRequester,
castCrewRowFocusRequester: FocusRequester,
guestStarRowFocusRequester: FocusRequester,
extrasRowFocusRequester: FocusRequester,
onChangeSeason: (Int) -> Unit,
onFocusEpisode: (Int) -> Unit,
onClick: (BaseItem) -> Unit,
@ -93,6 +96,7 @@ fun SeriesOverviewContent(
personOnClick: (Person) -> Unit,
canDelete: (BaseItem) -> Boolean,
deleteOnClick: (BaseItem) -> Unit,
onClickExtra: (Int, ExtrasItem) -> Unit,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
@ -365,6 +369,21 @@ fun SeriesOverviewContent(
}
}
}
AnimatedVisibility(
visible = seasonExtras.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut(),
) {
ExtrasRow(
extras = seasonExtras,
onClickItem = onClickExtra,
onLongClickItem = { _, _ -> },
modifier =
Modifier
.fillMaxWidth()
.focusRequester(extrasRowFocusRequester),
)
}
}
}
}

View file

@ -184,6 +184,10 @@ class SeriesViewModel
it.copy(seasonTabIndex = index.coerceAtLeast(0))
}
}
viewModelScope.launchIO {
val extras = extrasService.getExtras(seasonEpisodeIds.seasonId)
this@SeriesViewModel.extras.setValueOnMain(extras)
}
}
val remoteTrailers = trailerService.getRemoteTrailers(item)
withContext(Dispatchers.Main) {
@ -388,6 +392,7 @@ class SeriesViewModel
if (currentEpisodes == null || currentEpisodes.seasonId != seasonId) {
this@SeriesViewModel.peopleInEpisode.value = PeopleInItem()
this@SeriesViewModel.episodes.value = EpisodeList.Loading
this@SeriesViewModel.extras.value = emptyList()
}
viewModelScope.launchIO(ExceptionHandler(true)) {
val episodes =
@ -401,6 +406,10 @@ class SeriesViewModel
this@SeriesViewModel.episodes.value = episodes
}
}
viewModelScope.launchIO {
val extras = extrasService.getExtras(seasonId)
this@SeriesViewModel.extras.setValueOnMain(extras)
}
}
fun setWatched(