From c9cab7a5c2dda3e0c419608c9e9913fc6b8adf2e Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:34:38 -0400 Subject: [PATCH] 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 season_extras Large ## AI or LLM usage None --- .../ui/detail/series/SeriesOverview.kt | 14 +++++++++++-- .../ui/detail/series/SeriesOverviewContent.kt | 21 ++++++++++++++++++- .../ui/detail/series/SeriesViewModel.kt | 9 ++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 23633651..5762e55b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 34a9539a..d87d339c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -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, episodes: EpisodeList, + seasonExtras: List, chosenStreams: ChosenStreams?, peopleInEpisode: List, 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), + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index de04149b..c84f9b8d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -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(