mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Better date, time, & bitrate formatting, other small UI improvements (#1139)
## Description Use localized date & time formatting such as using 24 hour clock or Day-Month-Year date formatting. The localization is based on the device's locale/language setting. Some pages require reloading if you switch the locale while Wholphin is running. Format bitrates using SI 1000 magnitudes instead of 1024. File size still uses 1024 but formats with "MiB"/"GiB" suffixes now. This aligns with the server web UI and is more in line with standard practices for display as well. Fixes the playback overlay layout being messed up by extremely long episode names. Finally, adds jumping by letter using keyboard letter keys on grids if letter jumping is enabled. I doubt many users have a full keyboard attached to their TV, but this saves me time using the emulator when I need to repeatedly open the same item for testing. ### Related issues Fixes #1132 ### Testing Emulator ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
f2570d4ab0
commit
8f9b9813b7
9 changed files with 85 additions and 59 deletions
|
|
@ -5,13 +5,13 @@ 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.abbreviateNumber
|
||||
import com.github.damontecres.wholphin.ui.detail.CardGridItem
|
||||
import com.github.damontecres.wholphin.ui.detail.music.artistsString
|
||||
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.getDateFormatter
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playback.playable
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
|
|
@ -118,7 +118,7 @@ data class BaseItem(
|
|||
buildList {
|
||||
if (type == BaseItemKind.EPISODE) {
|
||||
data.seasonEpisode?.let(::add)
|
||||
data.premiereDate?.let { add(DateFormatter.format(it)) }
|
||||
data.premiereDate?.let { add(getDateFormatter().format(it)) }
|
||||
} else if (type == BaseItemKind.SERIES) {
|
||||
data.seriesProductionYears?.let(::add)
|
||||
} else if (type == BaseItemKind.PHOTO) {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,25 @@ import java.time.format.DateTimeParseException
|
|||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
val TimeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
val DateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("MMM d, yyyy")
|
||||
private var timeFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.getDefault())
|
||||
|
||||
fun getTimeFormatter(): DateTimeFormatter {
|
||||
if (timeFormatter.locale != Locale.getDefault()) {
|
||||
timeFormatter = timeFormatter.withLocale(Locale.getDefault())
|
||||
}
|
||||
return timeFormatter
|
||||
}
|
||||
|
||||
private var dateFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.getDefault())
|
||||
|
||||
fun getDateFormatter(): DateTimeFormatter {
|
||||
if (dateFormatter.locale != Locale.getDefault()) {
|
||||
dateFormatter = dateFormatter.withLocale(Locale.getDefault())
|
||||
}
|
||||
return dateFormatter
|
||||
}
|
||||
|
||||
// TODO server returns in UTC, but sdk converts to local time
|
||||
// eg 2020-02-14T00:00:00.0000000Z => 2020-02-13T17:00:00 PT => Feb 13, 2020
|
||||
|
|
@ -25,9 +42,9 @@ val DateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("MMM d, yyyy"
|
|||
/**
|
||||
* Format a [LocalDateTime] as `Aug 24, 2000`
|
||||
*/
|
||||
fun formatDateTime(dateTime: LocalDateTime): String = DateFormatter.format(dateTime)
|
||||
fun formatDateTime(dateTime: LocalDateTime): String = getDateFormatter().format(dateTime)
|
||||
|
||||
fun formatDate(dateTime: LocalDate): String = DateFormatter.format(dateTime)
|
||||
fun formatDate(dateTime: LocalDate): String = getDateFormatter().format(dateTime)
|
||||
|
||||
fun toLocalDate(date: String?): LocalDate? =
|
||||
date?.let {
|
||||
|
|
@ -109,30 +126,25 @@ fun abbreviateNumber(number: Int): String {
|
|||
return String.format(Locale.getDefault(), "%.1f%s", count, abbrevSuffixes[unit])
|
||||
}
|
||||
|
||||
val byteSuffixes = listOf("B", "KB", "MB", "GB", "TB")
|
||||
val byteSuffixes = listOf("B", "KiB", "MiB", "GiB", "TiB")
|
||||
val byteRateSuffixes = listOf("bps", "kbps", "mbps", "gbps", "tbps")
|
||||
|
||||
/**
|
||||
* Format bytes
|
||||
*/
|
||||
fun formatBytes(
|
||||
bytes: Int,
|
||||
suffixes: List<String> = byteSuffixes,
|
||||
) = formatBytes(bytes.toLong(), suffixes)
|
||||
|
||||
fun formatBytes(
|
||||
bytes: Long,
|
||||
suffixes: List<String> = byteSuffixes,
|
||||
divisor: Int = 1024,
|
||||
): String {
|
||||
var unit = 0
|
||||
var count = bytes.toDouble()
|
||||
while (count >= 1024 && unit + 1 < suffixes.size) {
|
||||
count /= 1024
|
||||
while (count >= divisor && unit + 1 < suffixes.size) {
|
||||
count /= divisor
|
||||
unit++
|
||||
}
|
||||
return String.format(Locale.getDefault(), "%.2f %s", count, suffixes[unit])
|
||||
}
|
||||
|
||||
fun formatBitrate(bitrate: Int) = formatBytes(bitrate.toLong(), byteRateSuffixes, 1000)
|
||||
|
||||
@get:StringRes
|
||||
val MediaSegmentType.stringRes: Int
|
||||
get() =
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import androidx.tv.material3.Icon
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.TimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.dot
|
||||
import com.github.damontecres.wholphin.ui.getTimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.util.LocalClock
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ fun TimeRemaining(
|
|||
val now by LocalClock.current.now
|
||||
val remainingStr =
|
||||
remember(remaining, now) {
|
||||
val endTimeStr = TimeFormatter.format(now.plusSeconds(remaining.inWholeSeconds))
|
||||
val endTimeStr = getTimeFormatter().format(now.plusSeconds(remaining.inWholeSeconds))
|
||||
buildAnnotatedString {
|
||||
dot()
|
||||
append(context.getString(R.string.ends_at, endTimeStr))
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.byteRateSuffixes
|
||||
import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
||||
import com.github.damontecres.wholphin.ui.formatBitrate
|
||||
import com.github.damontecres.wholphin.ui.formatBytes
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
|
|
@ -116,7 +116,7 @@ fun ItemDetailsDialog(
|
|||
}
|
||||
source.bitrate?.let {
|
||||
add(
|
||||
bitrateLabel to formatBytes(it, byteRateSuffixes),
|
||||
bitrateLabel to formatBitrate(it),
|
||||
)
|
||||
}
|
||||
source.runTimeTicks?.let {
|
||||
|
|
@ -297,7 +297,7 @@ private fun buildVideoStreamInfo(
|
|||
val aspectRatio = calculateAspectRatio(stream.width!!, stream.height!!)
|
||||
add(aspectRatioLabel to aspectRatio)
|
||||
}
|
||||
stream.bitRate?.let { add(bitrateLabel to formatBytes(it, byteRateSuffixes)) }
|
||||
stream.bitRate?.let { add(bitrateLabel to formatBitrate(it)) }
|
||||
stream.averageFrameRate?.let {
|
||||
add(framerateLabel to String.format(Locale.getDefault(), "%.3f", it))
|
||||
}
|
||||
|
|
@ -405,7 +405,7 @@ private fun buildAudioStreamInfo(
|
|||
stream.channelLayout?.let { add(layoutLabel to it) }
|
||||
stream.channels?.let { add(channelsLabel to it.toString()) }
|
||||
stream.profile?.let { add(profileLabel to it) }
|
||||
stream.bitRate?.let { add(bitrateLabel to formatBytes(it, byteRateSuffixes)) }
|
||||
stream.bitRate?.let { add(bitrateLabel to formatBitrate(it)) }
|
||||
stream.sampleRate?.let { add(sampleRateLabel to "$it $sampleRateUnit") }
|
||||
stream.isDefault?.let { add(defaultLabel to if (it) yesStr else noStr) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin.ui.detail
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -51,7 +52,6 @@ import androidx.compose.ui.input.key.key
|
|||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
|
@ -199,6 +199,25 @@ fun <T : CardGridItem> CardGrid(
|
|||
}
|
||||
}
|
||||
|
||||
val jumpToLetter: (Char) -> Unit =
|
||||
remember {
|
||||
{ letter: Char ->
|
||||
scope.launch(ExceptionHandler()) {
|
||||
val jumpPosition =
|
||||
withContext(Dispatchers.IO) {
|
||||
letterPosition.invoke(letter)
|
||||
}
|
||||
Timber.d("Alphabet jump to $jumpPosition")
|
||||
if (jumpPosition >= 0) {
|
||||
pager.getOrNull(jumpPosition)
|
||||
gridState.scrollToItem(jumpPosition)
|
||||
focusOn(jumpPosition)
|
||||
alphabetFocus = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pager.isEmpty()) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -257,6 +276,12 @@ fun <T : CardGridItem> CardGrid(
|
|||
} else if (useJumpRemoteButtons && isBackwardButton(it)) {
|
||||
jump(-jump1)
|
||||
return@onKeyEvent true
|
||||
} else if (showLetterButtons && pager.isNotEmpty() &&
|
||||
it.nativeKeyEvent.keyCode in (KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_Z)
|
||||
) {
|
||||
val letter = it.nativeKeyEvent.unicodeChar.toChar()
|
||||
jumpToLetter.invoke(letter)
|
||||
return@onKeyEvent true
|
||||
} else {
|
||||
return@onKeyEvent false
|
||||
}
|
||||
|
|
@ -372,8 +397,7 @@ fun <T : CardGridItem> CardGrid(
|
|||
}
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val letters = context.getString(R.string.jump_letters)
|
||||
val letters = stringResource(R.string.jump_letters)
|
||||
// Letters
|
||||
val currentLetter =
|
||||
remember(focusedIndex) {
|
||||
|
|
@ -383,12 +407,10 @@ fun <T : CardGridItem> CardGrid(
|
|||
?.firstOrNull()
|
||||
?.uppercaseChar()
|
||||
?.let {
|
||||
if (it >= '0' && it <= '9') {
|
||||
'#'
|
||||
} else if (it >= 'A' && it <= 'Z') {
|
||||
it
|
||||
} else {
|
||||
null
|
||||
when (it) {
|
||||
in '0'..'9' -> '#'
|
||||
in 'A'..'Z' -> it
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
?: letters[0]
|
||||
|
|
@ -402,21 +424,7 @@ fun <T : CardGridItem> CardGrid(
|
|||
.align(Alignment.CenterVertically)
|
||||
.padding(start = 16.dp),
|
||||
// Add end padding to push away from edge
|
||||
letterClicked = { letter ->
|
||||
scope.launch(ExceptionHandler()) {
|
||||
val jumpPosition =
|
||||
withContext(Dispatchers.IO) {
|
||||
letterPosition.invoke(letter)
|
||||
}
|
||||
Timber.d("Alphabet jump to $jumpPosition")
|
||||
if (jumpPosition >= 0) {
|
||||
pager.getOrNull(jumpPosition)
|
||||
gridState.scrollToItem(jumpPosition)
|
||||
focusOn(jumpPosition)
|
||||
alphabetFocus = true
|
||||
}
|
||||
}
|
||||
},
|
||||
letterClicked = jumpToLetter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ import com.github.damontecres.wholphin.services.MusicService
|
|||
import com.github.damontecres.wholphin.services.MusicServiceState
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.wholphin.ui.TimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemCardImage
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
|
||||
|
|
@ -95,6 +94,7 @@ import com.github.damontecres.wholphin.ui.detail.music.buildMoreDialogForMusic
|
|||
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.ifElse
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
|
|
@ -810,7 +810,7 @@ fun PlaylistItem(
|
|||
val endTimeStr =
|
||||
remember(item, now) {
|
||||
val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds)
|
||||
TimeFormatter.format(endTime)
|
||||
getTimeFormatter().format(endTime)
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.ProvideTextStyle
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||
import com.github.damontecres.wholphin.ui.byteRateSuffixes
|
||||
import com.github.damontecres.wholphin.ui.formatBytes
|
||||
import com.github.damontecres.wholphin.ui.formatBitrate
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
|
@ -104,7 +103,7 @@ fun TranscodeInfo(
|
|||
"Reason:" to info.transcodeReasons.joinToString(", "),
|
||||
"HW Accel:" to info.hardwareAccelerationType?.toString(),
|
||||
"Container:" to info.container,
|
||||
"Bitrate:" to info.bitrate?.let { formatBytes(it, byteRateSuffixes) },
|
||||
"Bitrate:" to info.bitrate?.let { formatBitrate(it) },
|
||||
),
|
||||
)
|
||||
SimpleTable(
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import androidx.compose.ui.input.key.type
|
|||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
|
|
@ -74,10 +75,10 @@ import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
|||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.TimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.cards.ChapterCard
|
||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
||||
import com.github.damontecres.wholphin.ui.getTimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -625,6 +626,9 @@ fun Controller(
|
|||
text = it,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontSize = titleTextSize,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.MiddleEllipsis,
|
||||
modifier = Modifier.fillMaxWidth(.75f),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
|
|
@ -639,6 +643,9 @@ fun Controller(
|
|||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontSize = subtitleTextSize,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.MiddleEllipsis,
|
||||
modifier = Modifier.fillMaxWidth(.75f),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -651,7 +658,7 @@ fun Controller(
|
|||
.toLong()
|
||||
.milliseconds
|
||||
val endTime = LocalTime.now().plusSeconds(remaining.inWholeSeconds)
|
||||
endTimeStr = TimeFormatter.format(endTime)
|
||||
endTimeStr = getTimeFormatter().format(endTime)
|
||||
delay(1.seconds)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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.TimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.getTimeFormatter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
|
@ -25,9 +25,9 @@ data class Clock(
|
|||
*/
|
||||
val now: MutableState<LocalDateTime> = mutableStateOf(LocalDateTime.now()),
|
||||
/**
|
||||
* The current time formatted as a string with [TimeFormatter]
|
||||
* The current time formatted as a string with [getTimeFormatter]
|
||||
*/
|
||||
val timeString: MutableState<String> = mutableStateOf(TimeFormatter.format(now.value)),
|
||||
val timeString: MutableState<String> = mutableStateOf(getTimeFormatter().format(now.value)),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
@ -37,7 +37,7 @@ fun ProvideLocalClock(content: @Composable () -> Unit) {
|
|||
withContext(Dispatchers.Default) {
|
||||
while (isActive) {
|
||||
val now = LocalDateTime.now()
|
||||
val time = TimeFormatter.format(now)
|
||||
val time = getTimeFormatter().format(now)
|
||||
clock.now.value = now
|
||||
clock.timeString.value = time
|
||||
delay(2_000)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue