diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt
index 50df6071..dd1236a4 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt
@@ -14,6 +14,7 @@ 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.formatDuration
+import com.github.damontecres.wholphin.ui.formatEpisodeNumber
import com.github.damontecres.wholphin.ui.getDateFormatter
import com.github.damontecres.wholphin.ui.joinNotBlank
import com.github.damontecres.wholphin.ui.nav.Destination
@@ -111,7 +112,7 @@ data class BaseItem(
val ui =
BaseItemUi(
episodeCornerText =
- data.indexNumber?.let { "E$it" }
+ data.indexNumber?.let { formatEpisodeNumber(it) }
?: data.premiereDate?.let(::formatDateTime),
episodeUnplayedCornerText =
if (type == BaseItemKind.SERIES ||
@@ -119,7 +120,7 @@ data class BaseItem(
type == BaseItemKind.EPISODE ||
type == BaseItemKind.BOX_SET
) {
- data.indexNumber?.let { "E$it" }
+ data.indexNumber?.let { formatEpisodeNumber(it) }
?: data.userData
?.unplayedItemCount
?.takeIf { it > 0 }
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt
index 4b2f0b8b..282c161e 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt
@@ -79,36 +79,74 @@ fun toLocalDate(date: String?): LocalDate? =
}
}
+// Falls back to the default format if the application is not initialized (e.g. unit tests)
+private fun formatStringWithFallback(
+ @StringRes resId: Int,
+ defaultFormat: String,
+ vararg args: Any,
+): String {
+ val resources =
+ try {
+ WholphinApplication.instance.resources
+ } catch (_: UninitializedPropertyAccessException) {
+ null
+ }
+ return resources?.getString(resId, *args)
+ ?: String.format(Locale.getDefault(), defaultFormat, *args)
+}
+
/**
- * If the item has season & episode info, format as `S# E#`
+ * Format season & episode numbers using localized string resources.
+ * Returns null if [season] or [episode] is missing.
+ */
+fun formatSeasonEpisode(
+ season: Int?,
+ episode: Int?,
+ episodeEnd: Int? = null,
+ padded: Boolean = false,
+): String? {
+ if (season == null || episode == null) return null
+ return if (episodeEnd != null) {
+ if (padded) {
+ formatStringWithFallback(R.string.season_episode_number_padded_range, "S%1\$02dE%2\$02d-E%3\$02d", season, episode, episodeEnd)
+ } else {
+ formatStringWithFallback(R.string.season_episode_number_range, "S%1\$d E%2\$d-E%3\$d", season, episode, episodeEnd)
+ }
+ } else if (padded) {
+ formatStringWithFallback(R.string.season_episode_number_padded, "S%1\$02dE%2\$02d", season, episode)
+ } else {
+ formatStringWithFallback(R.string.season_episode_number, "S%1\$d E%2\$d", season, episode)
+ }
+}
+
+/** Compact episode-only label, e.g. `E3`. */
+fun formatEpisodeNumber(episode: Int): String = formatStringWithFallback(R.string.episode_number_short, "E%1\$d", episode)
+
+/** Fallback season title when the library has no season name, e.g. `Season 2`. */
+fun formatSeasonNumber(season: Int): String = formatStringWithFallback(R.string.season_number, "Season %1\$d", season)
+
+/**
+ * If the item has season & episode info, format as localized `S# E#`
*/
val BaseItemDto.seasonEpisode: String?
get() =
- if (parentIndexNumber != null && indexNumber != null && indexNumberEnd != null) {
- "S$parentIndexNumber E$indexNumber-E$indexNumberEnd"
- } else if (parentIndexNumber != null && indexNumber != null) {
- "S$parentIndexNumber E$indexNumber"
- } else {
- null
- }
+ formatSeasonEpisode(
+ parentIndexNumber,
+ indexNumber,
+ indexNumberEnd,
+ )
/**
- * If the item has season & episode info, format padded as `S## E##`
+ * If the item has season & episode info, format padded as localized `S##E##`
*/
val BaseItemDto.seasonEpisodePadded: String?
get() =
- if (parentIndexNumber != null && indexNumber != null) {
- val season = parentIndexNumber?.toString()?.padStart(2, '0')
- val episode = indexNumber?.toString()?.padStart(2, '0')
- val endEpisode = indexNumberEnd?.toString()?.padStart(2, '0')
- if (endEpisode != null) {
- "S${season}E$episode-E$endEpisode"
- } else {
- "S${season}E$episode"
- }
- } else {
- null
- }
+ formatSeasonEpisode(
+ parentIndexNumber,
+ indexNumber,
+ indexNumberEnd,
+ padded = true,
+ )
val BaseItemDto.seriesProductionYears: String?
get() =
@@ -274,6 +312,7 @@ fun Resources.formatDuration(duration: Duration): String =
when {
hours > 0 && minutes > 0 && seconds > 0 -> getString(R.string.duration_hours_minutes_seconds, hours.toInt(), minutes, seconds)
hours > 0 && minutes > 0 -> getString(R.string.duration_hours_minutes, hours.toInt(), minutes)
+ hours > 0 && seconds > 0 -> getString(R.string.duration_hours_seconds, hours.toInt(), seconds)
hours > 0 -> getString(R.string.duration_hours, hours.toInt())
minutes > 0 && seconds > 0 -> getString(R.string.duration_minutes_seconds, minutes, seconds)
minutes > 0 -> getString(R.string.duration_minutes, minutes)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt
index c482484e..bc11825f 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt
@@ -46,6 +46,7 @@ import com.github.damontecres.wholphin.ui.cards.PendingIndicator
import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage
+import com.github.damontecres.wholphin.ui.formatSeasonNumber
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState
@@ -277,7 +278,8 @@ fun SeasonListItem(
Text(
text =
season.season.name
- ?: (stringResource(R.string.tv_season) + " ${season.season.seasonNumber}"),
+ ?: season.season.seasonNumber?.let { formatSeasonNumber(it) }
+ ?: "",
)
},
supportingContent = {
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt
index 41d53fd5..e5bf4e8f 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt
@@ -29,6 +29,7 @@ import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.FontAwesome
+import com.github.damontecres.wholphin.ui.formatSeasonEpisode
import java.time.LocalDateTime
@Composable
@@ -109,10 +110,15 @@ fun Program(
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
)
+ val seasonEpisodeLabel =
+ formatSeasonEpisode(
+ program.seasonEpisode?.season,
+ program.seasonEpisode?.episode,
+ )
val subtitle =
- remember(program) {
+ remember(program, seasonEpisodeLabel) {
listOfNotNull(
- program.seasonEpisode?.let { "S${it.season} E${it.episode}" },
+ seasonEpisodeLabel,
program.subtitle,
).joinToString(" - ").ifBlank { null }
}
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt
index 987aeebb..470f33c2 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt
@@ -22,6 +22,7 @@ import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
import com.github.damontecres.wholphin.ui.dot
import com.github.damontecres.wholphin.ui.formatDuration
+import com.github.damontecres.wholphin.ui.formatSeasonEpisode
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
@@ -636,7 +637,7 @@ data class TvProgram(
.roundMinutes
.let { append(resources.getString(R.string.time_left, resources.formatDuration(it))) }
}
- seasonEpisode?.let { "S${it.season} E${it.episode}" }?.let {
+ formatSeasonEpisode(seasonEpisode?.season, seasonEpisode?.episode)?.let {
dot()
append(it)
}
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 a3ca1358..5fee7b24 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
@@ -42,7 +42,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.nestedscroll.nestedScroll
-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
@@ -60,6 +59,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TabDetails
import com.github.damontecres.wholphin.ui.components.TabRow
import com.github.damontecres.wholphin.ui.components.TitleOrLogo
+import com.github.damontecres.wholphin.ui.formatSeasonNumber
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.logTab
import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp
@@ -121,7 +121,6 @@ fun SeriesOverviewContent(
val scrollConnection = rememberDelayedNestedScroll()
var requestFocusAfterSeason by remember { mutableStateOf(false) }
- val seasonStr = stringResource(R.string.tv_season)
val tabFocusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } }
val contentFocusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } }
val tabs =
@@ -130,7 +129,7 @@ fun SeriesOverviewContent(
title =
StringStringProvider(
season?.name
- ?: season?.data?.indexNumber?.let { "$seasonStr $it" }
+ ?: season?.data?.indexNumber?.let { formatSeasonNumber(it) }
?: "",
),
tabFocusRequester = tabFocusRequesters[index],
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 549614c7..e8ae0187 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -28,6 +28,7 @@
%.2f seconds
%1$dh %2$dm %3$ds
%1$dh %2$dm
+ %1$dh %2$ds
%1$dm %2$ds
%1$dh
%1$dm
@@ -158,6 +159,12 @@
Guide
Season
Seasons
+ S%1$d E%2$d
+ S%1$d E%2$d-E%3$d
+ S%1$02dE%2$02d
+ S%1$02dE%2$02d-E%3$02d
+ E%1$d
+ Season %1$d
TV Shows
Interface
Unknown