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
## 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(