Optimize quick details UI (#674)

## Description
This PR optimizes the rendering of "quick details" in headers. That's
the dot separated row of Season/Episode/Date/Runtime, etc

The UI is slightly different because I moved the "Ends at" part to the
end of the row. The dots & spacing is also slightly different.

In testing this on my CCwGTV HD, a pretty low powered device, the PR
improved the time to render by over 3x (59.42ms->17.72ms). Note: these
times are during profiling and are not real world performance, but the
relative change should be applicable in the real world.

### Dev details
- Changes the dot separated row of multiple Text & Box composables into
a row of two Texts
- The text is an `AnnotatedString` which is created on the IO thread
when `BaseItem` is created
- A separate composable creates the "Ends at" to prevent recompositions
of the other strings
This commit is contained in:
Ray 2026-01-14 17:07:17 -05:00 committed by GitHub
parent 3215326515
commit c9d1244057
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 339 additions and 386 deletions

View file

@ -1,20 +1,29 @@
package com.github.damontecres.wholphin.data.model
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import com.github.damontecres.wholphin.ui.DateFormatter
import com.github.damontecres.wholphin.ui.detail.CardGridItem
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.dot
import com.github.damontecres.wholphin.ui.formatDateTime
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playback.playable
import com.github.damontecres.wholphin.ui.roundMinutes
import com.github.damontecres.wholphin.ui.seasonEpisode
import com.github.damontecres.wholphin.ui.seasonEpisodePadded
import com.github.damontecres.wholphin.ui.seriesProductionYears
import com.github.damontecres.wholphin.ui.timeRemaining
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.extensions.ticks
import java.util.Locale
import kotlin.time.Duration
@Serializable
@ -73,6 +82,61 @@ data class BaseItem(
val favorite get() = data.userData?.isFavorite ?: false
@Transient
val timeRemainingOrRuntime: Duration? = data.timeRemaining ?: data.runTimeTicks?.ticks
@Transient
val ui =
BaseItemUi(
quickDetails =
buildAnnotatedString {
val details =
buildList {
if (type == BaseItemKind.EPISODE) {
data.seasonEpisode?.let(::add)
data.premiereDate?.let { add(DateFormatter.format(it)) }
} else if (type == BaseItemKind.SERIES) {
data.seriesProductionYears?.let(::add)
} else {
data.productionYear?.let { add(it.toString()) }
}
data.runTimeTicks
?.ticks
?.roundMinutes
?.let { add(it.toString()) }
data.timeRemaining
?.roundMinutes
?.let { add("$it left") }
}
details.forEachIndexed { index, string ->
append(string)
if (index != details.lastIndex) {
dot()
}
}
// TODO time remaining
data.officialRating?.let {
dot()
append(it)
}
data.communityRating?.let {
dot()
append(String.format(Locale.getDefault(), "%.1f", it))
appendInlineContent(id = "star")
}
data.criticRating?.let {
dot()
append("${it.toInt()}%")
if (it >= 60f) {
appendInlineContent(id = "fresh")
} else {
appendInlineContent(id = "rotten")
}
}
},
)
private fun dateAsIndex(): Int? =
data.premiereDate
?.let {
@ -124,3 +188,8 @@ data class BaseItem(
}
val BaseItemDto.aspectRatioFloat: Float? get() = width?.let { w -> height?.let { h -> w.toFloat() / h.toFloat() } }
@Immutable
data class BaseItemUi(
val quickDetails: AnnotatedString,
)