Better home page header for Seasons (#2016)

## Description
Updates the home page header & card text when a Season type is shown,
such as in recently added for a TV Show library.

The title or logo for the series is shown with the season name as the
title. The "quick details" has the year of the season. The card shows
the unplayed count of episodes.

### Related issues
Closes #1958

### Testing
Emulator over a few days to see different seasons

## Screenshots
<img width="313" height="153" alt="image"
src="https://github.com/user-attachments/assets/c41e9edd-9f2a-4f62-b76d-e452e83dd8bf"
/>

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-09-30 17:35:29 -04:00 • committed by GitHub
parent cf74736579
commit b0d270380d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 108 additions and 38 deletions

View file

@ -112,21 +112,31 @@ data class BaseItem(
val ui =
BaseItemUi(
episodeCornerText =
data.indexNumber?.let { formatEpisodeNumber(it) }
?: data.premiereDate?.let(::formatDateTime),
episodeUnplayedCornerText =
if (type == BaseItemKind.SERIES ||
type == BaseItemKind.SEASON ||
type == BaseItemKind.EPISODE ||
type == BaseItemKind.BOX_SET
) {
if (type == BaseItemKind.EPISODE) {
data.indexNumber?.let { formatEpisodeNumber(it) }
?: data.userData
?: data.premiereDate?.let(::formatDateTime)
} else {
null
},
episodeUnplayedCornerText =
when (type) {
BaseItemKind.EPISODE -> {
data.indexNumber?.let { formatEpisodeNumber(it) }
}
BaseItemKind.SERIES,
BaseItemKind.SEASON,
BaseItemKind.BOX_SET,
-> {
data.userData
?.unplayedItemCount
?.takeIf { it > 0 }
?.let { abbreviateNumber(it) }
} else {
null
}
else -> {
null
}
},
quickDetails =
QuickDetailsData(
@ -134,33 +144,64 @@ data class BaseItem(
buildAnnotatedString {
val details =
buildList {
if (type == BaseItemKind.EPISODE) {
data.seasonEpisode?.let(::add)
data.premiereDate?.let { add(getDateFormatter().format(it)) }
} else if (type == BaseItemKind.SERIES) {
data.seriesProductionYears?.let(::add)
} else if (type == BaseItemKind.PHOTO) {
if (data.productionYear != null) {
add(data.productionYear!!.toString())
} else if (data.premiereDate != null) {
add(data.premiereDate!!.toLocalDate().toString())
}
} else if (type == BaseItemKind.BOX_SET) {
data.productionYear?.let { add(it.toString()) }
data.childCount?.let { add("$it items") }
} else if (type == BaseItemKind.PROGRAM) {
data.channelName?.let(::add)
if (data.isSeries == true) {
// TV episode
when (type) {
BaseItemKind.EPISODE -> {
data.seasonEpisode?.let(::add)
data.premiereDate?.let {
add(getDateFormatter().format(it))
add(
getDateFormatter().format(
it,
),
)
}
}
BaseItemKind.SEASON -> {
data.childCount?.let { childCount ->
add(
WholphinApplication.instance.resources.getQuantityString(
R.plurals.episodes_count,
childCount,
childCount,
),
)
}
} else {
data.productionYear?.let { add(it.toString()) }
}
} else {
data.productionYear?.let { add(it.toString()) }
BaseItemKind.SERIES -> {
data.seriesProductionYears?.let(::add)
}
BaseItemKind.PHOTO -> {
if (data.productionYear != null) {
add(data.productionYear!!.toString())
} else if (data.premiereDate != null) {
add(data.premiereDate!!.toLocalDate().toString())
}
}
BaseItemKind.BOX_SET -> {
data.productionYear?.let { add(it.toString()) }
data.childCount?.let { add("$it items") }
}
BaseItemKind.PROGRAM -> {
data.channelName?.let(::add)
if (data.isSeries == true) {
// TV episode
data.seasonEpisode?.let(::add)
data.premiereDate?.let {
add(getDateFormatter().format(it))
}
} else {
data.productionYear?.let { add(it.toString()) }
}
}
else -> {
data.productionYear?.let { add(it.toString()) }
}
}
data.runTimeTicks
?.ticks

View file

@ -212,6 +212,8 @@ val CollectionType.baseItemKinds: List<BaseItemKind>
CollectionType.HOMEVIDEOS -> {
listOf(BaseItemKind.VIDEO)
listOf(BaseItemKind.PHOTO)
listOf(BaseItemKind.PHOTO_ALBUM)
}
CollectionType.MUSIC -> {

View file

@ -91,7 +91,10 @@ fun TitleOrLogo(
fun rememberLogoUrl(item: BaseItem?): String? {
val imageUrlService = LocalImageUrlService.current
return remember(item?.id) {
if (item?.type == BaseItemKind.EPISODE && item.data.seriesId != null && item.data.parentLogoImageTag != null) {
if ((item?.type == BaseItemKind.EPISODE || item?.type == BaseItemKind.SEASON) &&
item.data.seriesId != null &&
item.data.parentLogoImageTag != null
) {
imageUrlService.getItemImageUrl(item.data.seriesId!!, ImageType.LOGO)
} else if (ImageType.LOGO in item?.data?.imageTags.orEmpty()) {
imageUrlService.getItemImageUrl(item, ImageType.LOGO)

View file

@ -545,11 +545,30 @@ fun HomePageHeader(
modifier: Modifier = Modifier,
) {
val isEpisode = item?.type == BaseItemKind.EPISODE
val dto = item?.data
val title =
remember(item?.id) {
when (item?.type) {
BaseItemKind.EPISODE,
BaseItemKind.SEASON,
-> item.data.seriesName
else -> item?.title
}
}
val subtitle =
remember(item?.id) {
when (item?.type) {
BaseItemKind.SEASON,
BaseItemKind.EPISODE,
-> item.name
else -> null
}
}
HomePageHeader(
title = item?.title,
subtitle = if (isEpisode) dto?.name else null,
overview = dto?.overview,
title = title,
subtitle = subtitle,
overview = item?.data?.overview,
overviewTwoLines = isEpisode,
quickDetails = item?.ui?.quickDetails,
timeRemaining = item?.timeRemainingOrRuntime,

View file

@ -927,4 +927,9 @@
<string name="remove_from_playlist">Remove from playlist</string>
<string name="login_credentials_expired">Credentials expired, please login in again</string>
<string name="login_not_authorized">You are not authorized to access the server at this time</string>
<plurals name="episodes_count">
<item quantity="zero">%1$d Episodes</item>
<item quantity="one">%1$d Episode</item>
<item quantity="other">%1$d Episodes</item>
</plurals>
</resources>