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 package com.github.damontecres.wholphin.ui
import android.content.Context
import android.text.format.DateFormat
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -12,19 +14,35 @@ import org.jellyfin.sdk.model.api.MediaSegmentType
import timber.log.Timber import timber.log.Timber
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException import java.time.format.DateTimeParseException
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Date
import java.util.Locale 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 { fun formatTime(
if (timeFormatter.locale != Locale.getDefault()) { context: Context,
timeFormatter = timeFormatter.withLocale(Locale.getDefault()) time: LocalTime,
} ): String {
return timeFormatter val instant =
time
.atDate(LocalDate.now())
.atZone(ZoneId.systemDefault())
.toInstant()
return DateFormat.getTimeFormat(context).format(Date.from(instant))
} }
private var dateFormatter: DateTimeFormatter = private var dateFormatter: DateTimeFormatter =

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString 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.data.model.QuickDetailsData
import com.github.damontecres.wholphin.preferences.DisplayToggle import com.github.damontecres.wholphin.preferences.DisplayToggle
import com.github.damontecres.wholphin.ui.dot 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.LocalClock
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
import org.jellyfin.sdk.model.DateTime import org.jellyfin.sdk.model.DateTime
@ -138,21 +139,11 @@ fun TimeRemaining(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.titleSmall, textStyle: TextStyle = MaterialTheme.typography.titleSmall,
) { ) {
val resources = LocalResources.current
val now by LocalClock.current.now val now by LocalClock.current.now
val remainingStr = EndsAt(
remember(remaining, now, resources) { endsAt = now.plusSeconds(remaining.inWholeSeconds),
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,
modifier = modifier, modifier = modifier,
textStyle = textStyle,
) )
} }
@ -163,9 +154,10 @@ fun EndsAt(
textStyle: TextStyle = MaterialTheme.typography.titleSmall, textStyle: TextStyle = MaterialTheme.typography.titleSmall,
) { ) {
val resources = LocalResources.current val resources = LocalResources.current
val context = LocalContext.current
val remainingStr = val remainingStr =
remember(endsAt, resources) { remember(endsAt, resources, context) {
val endTimeStr = getTimeFormatter().format(endsAt) val endTimeStr = formatTime(context, endsAt)
buildAnnotatedString { buildAnnotatedString {
dot() dot()
append(resources.getString(R.string.ends_at, endTimeStr)) 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.enableMarquee
import com.github.damontecres.wholphin.ui.equalsNotNull import com.github.damontecres.wholphin.ui.equalsNotNull
import com.github.damontecres.wholphin.ui.formatDateTime 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.ifElse
import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
@ -803,10 +803,11 @@ fun PlaylistItem(
trailingContent = { trailingContent = {
item?.data?.runTimeTicks?.ticks?.roundMinutes?.let { duration -> item?.data?.runTimeTicks?.ticks?.roundMinutes?.let { duration ->
val now by LocalClock.current.now val now by LocalClock.current.now
val context = LocalContext.current
val endTimeStr = val endTimeStr =
remember(item, now) { remember(item, now, context) {
val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds) val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds)
getTimeFormatter().format(endTime) formatTime(context, endTime)
} }
Column { Column {
Text( Text(

View file

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

View file

@ -7,7 +7,8 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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 com.github.damontecres.wholphin.util.WholphinDispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -25,19 +26,20 @@ data class Clock(
*/ */
val now: MutableState<LocalDateTime> = mutableStateOf(LocalDateTime.now()), 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 @Composable
fun ProvideLocalClock(content: @Composable () -> Unit) { fun ProvideLocalClock(content: @Composable () -> Unit) {
val context = LocalContext.current
val clock = remember { Clock() } val clock = remember { Clock() }
LaunchedEffect(Unit) { LaunchedEffect(context) {
withContext(WholphinDispatchers.Default) { withContext(WholphinDispatchers.Default) {
while (isActive) { while (isActive) {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val time = getTimeFormatter().format(now) val time = formatTime(context, now)
clock.now.value = now clock.now.value = now
clock.timeString.value = time clock.timeString.value = time
delay(2_000) delay(2_000)