mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-05 18:30:59 +01:00
Localized formatting for season and episode display (#1884)
## Description Added localized formatting for season/episode numbers across the UI because it was in English-only in every UI language. ### Related issues None ### Testing Tested on a physical Google TV Streamer 4K device. Checked manually relevant screens while browsing and during playback. ## Screenshots No frontend changes are visible in English and new translations will be contributed through Weblate for other languages later, as usual. ## AI or LLM usage Generated initial code with AI, then manually passed over it while cleaning and refactoring where needed. --------- Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
parent
826bbad49c
commit
1f3a861c47
7 changed files with 85 additions and 30 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
<string name="decimal_seconds">%.2f seconds</string>
|
||||
<string name="duration_hours_minutes_seconds">%1$dh %2$dm %3$ds</string>
|
||||
<string name="duration_hours_minutes">%1$dh %2$dm</string>
|
||||
<string name="duration_hours_seconds">%1$dh %2$ds</string>
|
||||
<string name="duration_minutes_seconds">%1$dm %2$ds</string>
|
||||
<string name="duration_hours">%1$dh</string>
|
||||
<string name="duration_minutes">%1$dm</string>
|
||||
|
|
@ -158,6 +159,12 @@
|
|||
<string name="tv_guide">Guide</string>
|
||||
<string name="tv_season">Season</string>
|
||||
<string name="tv_seasons">Seasons</string>
|
||||
<string name="season_episode_number">S%1$d E%2$d</string>
|
||||
<string name="season_episode_number_range">S%1$d E%2$d-E%3$d</string>
|
||||
<string name="season_episode_number_padded">S%1$02dE%2$02d</string>
|
||||
<string name="season_episode_number_padded_range">S%1$02dE%2$02d-E%3$02d</string>
|
||||
<string name="episode_number_short">E%1$d</string>
|
||||
<string name="season_number">Season %1$d</string>
|
||||
<string name="tv_shows_title">TV Shows</string>
|
||||
<string name="ui_interface">Interface</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue