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 7ab11e4b..41c1fd45 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 @@ -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) { 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 8f4066bb..0a9b9a2c 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 @@ -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 = byteSuffixes, -) = formatBytes(bytes.toLong(), suffixes) - fun formatBytes( bytes: Long, suffixes: List = 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]) + 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() = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt index cf5b0697..d4931d77 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt @@ -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)) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt index fa32c595..072de4e1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt @@ -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) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index ac72a144..a2b9e923 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -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 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 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 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 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 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, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index 9c237d8c..bd50ce59 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDebugOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDebugOverlay.kt index ce31d865..e756a9df 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDebugOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDebugOverlay.kt @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt index 2c536d9f..3bc4ddb0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt @@ -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) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt index bca5ae64..213dc725 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt @@ -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 = mutableStateOf(LocalDateTime.now()), /** - * The current time formatted as a string with [TimeFormatter] + * The current time formatted as a string with [getTimeFormatter] */ - val timeString: MutableState = mutableStateOf(TimeFormatter.format(now.value)), + val timeString: MutableState = 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)