Respect system 24-hour setting in clock time format (#1619)

## Description
<!-- Describe the changes in detail -->
The clock ignored the device's system 24-hour preference:
getTimeFormatter derived 12/24h purely from the locale. This reads
DateFormat.is24HourFormat and builds the time pattern from it, so the
clock follows the system setting.

### Related issues
<!-- If this is a new feature or a change, there must be a discussion in
an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug,
including steps to reproduce -->

### Testing
<!-- Describe how this change was tested and on what device(s) -->
Tested on the emulator toggling the system 24-hour setting on/off.
assembleDefaultDebug, testDefaultDebugUnitTest and pre-commit (ktlint)
pass.

## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->

## AI or LLM usage
<!-- If you used any AI or LLM assistance, please list where in the code
and how you tested it -->
The analysis and the fix were done by me with AI assistance (Claude). I
wrote the description myself; the English wording was polished with AI.
I understand the code and can maintain it.
This commit is contained in:
Christian Tismer-Sperling 2026-06-24 23:02:53 +02:00 • committed by GitHub
parent d705a3ea01
commit 8a684545ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 49 additions and 34 deletions

View file

@ -1,5 +1,7 @@
package com.github.damontecres.wholphin.ui
import android.content.Context
import android.text.format.DateFormat
import androidx.annotation.StringRes
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.ui.text.AnnotatedString
@ -12,19 +14,35 @@ import org.jellyfin.sdk.model.api.MediaSegmentType
import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.time.format.FormatStyle
import java.util.Date
import java.util.Locale
private var timeFormatter: DateTimeFormatter =
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.getDefault())
/**
* Format the time-of-day of [time] honouring the device's 12/24-hour setting and the current
* locale. [context] should be an Activity/composition context (e.g. `LocalContext.current`) so an
* in-app language switch (`AppCompatDelegate.setApplicationLocales`) is reflected; the application
* context may carry a stale locale.
*/
fun formatTime(
context: Context,
time: LocalDateTime,
): String = formatTime(context, time.toLocalTime())
fun getTimeFormatter(): DateTimeFormatter {
if (timeFormatter.locale != Locale.getDefault()) {
timeFormatter = timeFormatter.withLocale(Locale.getDefault())
}
return timeFormatter
fun formatTime(
context: Context,
time: LocalTime,
): String {
val instant =
time
.atDate(LocalDate.now())
.atZone(ZoneId.systemDefault())
.toInstant()
return DateFormat.getTimeFormat(context).format(Date.from(instant))
}
private var dateFormatter: DateTimeFormatter =

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
@ -25,7 +26,7 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.QuickDetailsData
import com.github.damontecres.wholphin.preferences.DisplayToggle
import com.github.damontecres.wholphin.ui.dot
import com.github.damontecres.wholphin.ui.getTimeFormatter
import com.github.damontecres.wholphin.ui.formatTime
import com.github.damontecres.wholphin.ui.util.LocalClock
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
import org.jellyfin.sdk.model.DateTime
@ -138,21 +139,11 @@ fun TimeRemaining(
modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
) {
val resources = LocalResources.current
val now by LocalClock.current.now
val remainingStr =
remember(remaining, now, resources) {
val endTimeStr = getTimeFormatter().format(now.plusSeconds(remaining.inWholeSeconds))
buildAnnotatedString {
dot()
append(resources.getString(R.string.ends_at, endTimeStr))
}
}
Text(
text = remainingStr,
style = textStyle,
maxLines = 1,
EndsAt(
endsAt = now.plusSeconds(remaining.inWholeSeconds),
modifier = modifier,
textStyle = textStyle,
)
}
@ -163,9 +154,10 @@ fun EndsAt(
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
) {
val resources = LocalResources.current
val context = LocalContext.current
val remainingStr =
remember(endsAt, resources) {
val endTimeStr = getTimeFormatter().format(endsAt)
remember(endsAt, resources, context) {
val endTimeStr = formatTime(context, endsAt)
buildAnnotatedString {
dot()
append(resources.getString(R.string.ends_at, endTimeStr))

View file

@ -94,7 +94,7 @@ import com.github.damontecres.wholphin.ui.detail.music.MusicViewModel
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.formatDateTime
import com.github.damontecres.wholphin.ui.getTimeFormatter
import com.github.damontecres.wholphin.ui.formatTime
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
@ -803,10 +803,11 @@ fun PlaylistItem(
trailingContent = {
item?.data?.runTimeTicks?.ticks?.roundMinutes?.let { duration ->
val now by LocalClock.current.now
val context = LocalContext.current
val endTimeStr =
remember(item, now) {
remember(item, now, context) {
val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds)
getTimeFormatter().format(endTime)
formatTime(context, endTime)
}
Column {
Text(

View file

@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
@ -32,7 +33,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.getTimeFormatter
import com.github.damontecres.wholphin.ui.formatTime
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
import com.github.damontecres.wholphin.ui.playback.PlaybackDialogType
import kotlinx.coroutines.delay
@ -199,8 +200,9 @@ fun Controller(
)
}
val context = LocalContext.current
var endTimeStr by remember { mutableStateOf("...") }
LaunchedEffect(player) {
LaunchedEffect(player, context) {
while (isActive) {
val remaining =
(player.duration - player.currentPosition)
@ -208,7 +210,7 @@ fun Controller(
.toLong()
.milliseconds
val endTime = LocalTime.now().plusSeconds(remaining.inWholeSeconds)
endTimeStr = getTimeFormatter().format(endTime)
endTimeStr = formatTime(context, endTime)
delay(1.seconds)
}
}

View file

@ -7,7 +7,8 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.github.damontecres.wholphin.ui.getTimeFormatter
import androidx.compose.ui.platform.LocalContext
import com.github.damontecres.wholphin.ui.formatTime
import com.github.damontecres.wholphin.util.WholphinDispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
@ -25,19 +26,20 @@ data class Clock(
*/
val now: MutableState<LocalDateTime> = mutableStateOf(LocalDateTime.now()),
/**
* The current time formatted as a string with [getTimeFormatter]
* The current time formatted as a string; populated by [ProvideLocalClock] via [formatTime].
*/
val timeString: MutableState<String> = mutableStateOf(getTimeFormatter().format(now.value)),
val timeString: MutableState<String> = mutableStateOf(""),
)
@Composable
fun ProvideLocalClock(content: @Composable () -> Unit) {
val context = LocalContext.current
val clock = remember { Clock() }
LaunchedEffect(Unit) {
LaunchedEffect(context) {
withContext(WholphinDispatchers.Default) {
while (isActive) {
val now = LocalDateTime.now()
val time = getTimeFormatter().format(now)
val time = formatTime(context, now)
clock.now.value = now
clock.timeString.value = time
delay(2_000)