mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Optimize quick details UI (#674)
## Description This PR optimizes the rendering of "quick details" in headers. That's the dot separated row of Season/Episode/Date/Runtime, etc The UI is slightly different because I moved the "Ends at" part to the end of the row. The dots & spacing is also slightly different. In testing this on my CCwGTV HD, a pretty low powered device, the PR improved the time to render by over 3x (59.42ms->17.72ms). Note: these times are during profiling and are not real world performance, but the relative change should be applicable in the real world. ### Dev details - Changes the dot separated row of multiple Text & Box composables into a row of two Texts - The text is an `AnnotatedString` which is created on the IO thread when `BaseItem` is created - A separate composable creates the "Ends at" to prevent recompositions of the other strings
This commit is contained in:
parent
3215326515
commit
c9d1244057
18 changed files with 339 additions and 386 deletions
|
|
@ -1,20 +1,29 @@
|
|||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
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.detail.CardGridItem
|
||||
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.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playback.playable
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.seasonEpisodePadded
|
||||
import com.github.damontecres.wholphin.ui.seriesProductionYears
|
||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Serializable
|
||||
|
|
@ -73,6 +82,61 @@ data class BaseItem(
|
|||
|
||||
val favorite get() = data.userData?.isFavorite ?: false
|
||||
|
||||
@Transient
|
||||
val timeRemainingOrRuntime: Duration? = data.timeRemaining ?: data.runTimeTicks?.ticks
|
||||
|
||||
@Transient
|
||||
val ui =
|
||||
BaseItemUi(
|
||||
quickDetails =
|
||||
buildAnnotatedString {
|
||||
val details =
|
||||
buildList {
|
||||
if (type == BaseItemKind.EPISODE) {
|
||||
data.seasonEpisode?.let(::add)
|
||||
data.premiereDate?.let { add(DateFormatter.format(it)) }
|
||||
} else if (type == BaseItemKind.SERIES) {
|
||||
data.seriesProductionYears?.let(::add)
|
||||
} else {
|
||||
data.productionYear?.let { add(it.toString()) }
|
||||
}
|
||||
data.runTimeTicks
|
||||
?.ticks
|
||||
?.roundMinutes
|
||||
?.let { add(it.toString()) }
|
||||
data.timeRemaining
|
||||
?.roundMinutes
|
||||
?.let { add("$it left") }
|
||||
}
|
||||
details.forEachIndexed { index, string ->
|
||||
append(string)
|
||||
if (index != details.lastIndex) {
|
||||
dot()
|
||||
}
|
||||
}
|
||||
// TODO time remaining
|
||||
|
||||
data.officialRating?.let {
|
||||
dot()
|
||||
append(it)
|
||||
}
|
||||
data.communityRating?.let {
|
||||
dot()
|
||||
append(String.format(Locale.getDefault(), "%.1f", it))
|
||||
appendInlineContent(id = "star")
|
||||
}
|
||||
data.criticRating?.let {
|
||||
dot()
|
||||
append("${it.toInt()}%")
|
||||
if (it >= 60f) {
|
||||
appendInlineContent(id = "fresh")
|
||||
} else {
|
||||
appendInlineContent(id = "rotten")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun dateAsIndex(): Int? =
|
||||
data.premiereDate
|
||||
?.let {
|
||||
|
|
@ -124,3 +188,8 @@ data class BaseItem(
|
|||
}
|
||||
|
||||
val BaseItemDto.aspectRatioFloat: Float? get() = width?.let { w -> height?.let { h -> w.toFloat() / h.toFloat() } }
|
||||
|
||||
@Immutable
|
||||
data class BaseItemUi(
|
||||
val quickDetails: AnnotatedString,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package com.github.damontecres.wholphin.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import com.github.damontecres.wholphin.R
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||
|
|
@ -152,3 +155,31 @@ val MediaSegmentType.skipStringRes: Int
|
|||
MediaSegmentType.OUTRO -> R.string.skip_segment_outro
|
||||
MediaSegmentType.INTRO -> R.string.skip_segment_intro
|
||||
}
|
||||
|
||||
fun AnnotatedString.Builder.dot() = append(" \u2022 ")
|
||||
|
||||
fun listToDotString(
|
||||
strings: List<String>,
|
||||
communityRating: Float?,
|
||||
criticRating: Float?,
|
||||
): AnnotatedString =
|
||||
buildAnnotatedString {
|
||||
strings.forEachIndexed { index, string ->
|
||||
append(string)
|
||||
if (index != strings.lastIndex) dot()
|
||||
communityRating?.let {
|
||||
dot()
|
||||
append(String.format(Locale.getDefault(), "%.1f", it))
|
||||
appendInlineContent(id = "star")
|
||||
}
|
||||
criticRating?.let {
|
||||
dot()
|
||||
append("${it.toInt()}%")
|
||||
if (it >= 60f) {
|
||||
appendInlineContent(id = "fresh")
|
||||
} else {
|
||||
appendInlineContent(id = "rotten")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.LocalTextStyle
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
|
||||
@Composable
|
||||
fun DotSeparatedRow(
|
||||
texts: List<String>,
|
||||
communityRating: Float? = null,
|
||||
criticRating: Float? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
||||
) {
|
||||
CompositionLocalProvider(LocalTextStyle provides textStyle) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
texts.forEachIndexed { index, text ->
|
||||
Text(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
)
|
||||
if (communityRating != null || criticRating != null || index != texts.lastIndex) {
|
||||
Dot()
|
||||
}
|
||||
}
|
||||
val height = with(LocalDensity.current) { textStyle.fontSize.toDp() }
|
||||
communityRating?.let {
|
||||
SimpleStarRating(
|
||||
communityRating = it,
|
||||
modifier = Modifier.height(height),
|
||||
)
|
||||
if (criticRating != null) {
|
||||
Dot()
|
||||
}
|
||||
}
|
||||
criticRating?.let {
|
||||
TomatoRating(it, Modifier.height(height))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Dot(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 1f))
|
||||
.size(4.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewTvSpec
|
||||
@Composable
|
||||
private fun DotSeparatedRowPreview() {
|
||||
WholphinTheme {
|
||||
Column {
|
||||
DotSeparatedRow(
|
||||
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"),
|
||||
communityRating = null,
|
||||
criticRating = .75f,
|
||||
modifier = Modifier,
|
||||
textStyle = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
DotSeparatedRow(
|
||||
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left"),
|
||||
communityRating = 7.5f,
|
||||
criticRating = .75f,
|
||||
modifier = Modifier,
|
||||
textStyle = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
DotSeparatedRow(
|
||||
texts = listOf("2025", "1h 48m", "PG-13", "1h 30m left 7.5"),
|
||||
communityRating = 7.5f,
|
||||
criticRating = .45f,
|
||||
modifier = Modifier,
|
||||
textStyle = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.ui.TimeFormatter
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||
import com.github.damontecres.wholphin.ui.util.LocalClock
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Composable
|
||||
fun MovieQuickDetails(
|
||||
dto: BaseItemDto?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val now by LocalClock.current.now
|
||||
val details =
|
||||
remember(dto, now) {
|
||||
buildList {
|
||||
dto?.productionYear?.let { add(it.toString()) }
|
||||
addRuntimeDetails(context, now, dto)
|
||||
dto?.officialRating?.let(::add)
|
||||
}
|
||||
}
|
||||
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = dto?.communityRating,
|
||||
criticRating = dto?.criticRating,
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
fun MutableList<String>.addRuntimeDetails(
|
||||
context: Context,
|
||||
now: LocalDateTime,
|
||||
dto: BaseItemDto?,
|
||||
) {
|
||||
val runtime = dto?.runTimeTicks?.ticks
|
||||
runtime?.let { duration ->
|
||||
add(duration.roundMinutes.toString())
|
||||
}
|
||||
dto?.timeRemaining?.roundMinutes?.let {
|
||||
add("$it left")
|
||||
}
|
||||
(dto?.timeRemaining ?: runtime)?.let { remaining ->
|
||||
val endTimeStr = TimeFormatter.format(now.plusSeconds(remaining.inWholeSeconds))
|
||||
add(context.getString(R.string.ends_at, endTimeStr))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.runtime.Composable
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
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.util.LocalClock
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Composable
|
||||
fun QuickDetails(
|
||||
details: AnnotatedString,
|
||||
timeRemaining: Duration?,
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
||||
) {
|
||||
val inlineContentMap =
|
||||
remember(textStyle) {
|
||||
mapOf(
|
||||
"star" to
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
textStyle.fontSize,
|
||||
textStyle.fontSize,
|
||||
PlaceholderVerticalAlign.TextCenter,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Star,
|
||||
tint = FilledStarColor,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
},
|
||||
"rotten" to
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
textStyle.fontSize,
|
||||
textStyle.fontSize,
|
||||
PlaceholderVerticalAlign.TextCenter,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_rotten_tomatoes_rotten),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
},
|
||||
"fresh" to
|
||||
InlineTextContent(
|
||||
Placeholder(
|
||||
textStyle.fontSize,
|
||||
textStyle.fontSize,
|
||||
PlaceholderVerticalAlign.TextCenter,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_rotten_tomatoes_fresh),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = modifier) {
|
||||
Text(
|
||||
text = details,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = textStyle,
|
||||
inlineContent = inlineContentMap,
|
||||
maxLines = 1,
|
||||
modifier = Modifier,
|
||||
)
|
||||
timeRemaining?.let { TimeRemaining(it, textStyle = textStyle) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimeRemaining(
|
||||
remaining: Duration,
|
||||
modifier: Modifier = Modifier,
|
||||
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val now by LocalClock.current.now
|
||||
val remainingStr =
|
||||
remember(remaining, now) {
|
||||
val endTimeStr = TimeFormatter.format(now.plusSeconds(remaining.inWholeSeconds))
|
||||
buildAnnotatedString {
|
||||
dot()
|
||||
append(context.getString(R.string.ends_at, endTimeStr))
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = remainingStr,
|
||||
style = textStyle,
|
||||
maxLines = 1,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,22 +1,13 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.seriesProductionYears
|
||||
import com.github.damontecres.wholphin.ui.util.LocalClock
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
|
||||
@Composable
|
||||
fun SeriesName(
|
||||
|
|
@ -55,52 +46,3 @@ fun EpisodeName(
|
|||
episode: BaseItemDto?,
|
||||
modifier: Modifier = Modifier,
|
||||
) = EpisodeName(episode?.episodeTitle ?: episode?.name, modifier)
|
||||
|
||||
@Composable
|
||||
fun EpisodeQuickDetails(
|
||||
dto: BaseItemDto?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val now by LocalClock.current.now
|
||||
val details =
|
||||
remember(dto, now) {
|
||||
buildList {
|
||||
dto?.seasonEpisode?.let(::add)
|
||||
dto?.premiereDate?.let { add(formatDateTime(it)) }
|
||||
addRuntimeDetails(context, now, dto)
|
||||
dto?.officialRating?.let(::add)
|
||||
}
|
||||
}
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = dto?.communityRating,
|
||||
criticRating = dto?.criticRating,
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SeriesQuickDetails(
|
||||
dto: BaseItemDto?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val details =
|
||||
remember(dto) {
|
||||
buildList {
|
||||
dto?.seriesProductionYears?.let(::add)
|
||||
dto?.runTimeTicks?.ticks?.roundMinutes?.let {
|
||||
add(it.toString())
|
||||
}
|
||||
dto?.officialRating?.let(::add)
|
||||
}
|
||||
}
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = dto?.communityRating,
|
||||
criticRating = dto?.criticRating,
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ import com.github.damontecres.wholphin.R
|
|||
import com.github.damontecres.wholphin.api.seerr.model.MovieDetails
|
||||
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.listToDotString
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -88,16 +89,16 @@ fun DiscoverMovieDetailsHeader(
|
|||
?.firstOrNull()
|
||||
?.certification
|
||||
?.let(::add)
|
||||
}.let {
|
||||
listToDotString(
|
||||
it,
|
||||
rating?.audienceRating,
|
||||
rating?.criticRating?.toFloat(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = rating?.audienceRating,
|
||||
criticRating = rating?.criticRating?.toFloat(),
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier,
|
||||
)
|
||||
QuickDetails(details, null)
|
||||
movie.genres?.mapNotNull { it.name }?.letNotEmpty {
|
||||
GenreText(it, Modifier.padding(bottom = padding))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.discover
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import com.github.damontecres.wholphin.data.model.DiscoverItem
|
||||
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun DiscoverQuickDetails(
|
||||
item: DiscoverItem?,
|
||||
rating: DiscoverRating?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Timber.v("id=${item?.id}, rating=$rating")
|
||||
val context = LocalContext.current
|
||||
val details =
|
||||
remember(item) {
|
||||
buildList {
|
||||
item
|
||||
?.releaseDate
|
||||
?.year
|
||||
?.toString()
|
||||
?.let(::add)
|
||||
}
|
||||
}
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = rating?.audienceRating,
|
||||
criticRating = rating?.criticRating?.toFloat(),
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -53,14 +53,15 @@ import com.github.damontecres.wholphin.ui.cards.ItemRow
|
|||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.listToDotString
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
|
|
@ -478,16 +479,16 @@ fun DiscoverSeriesDetailsHeader(
|
|||
?.toString()
|
||||
?.let(::add)
|
||||
// TODO
|
||||
}.let {
|
||||
listToDotString(
|
||||
it,
|
||||
rating?.audienceRating,
|
||||
rating?.criticRating?.toFloat(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = rating?.audienceRating,
|
||||
criticRating = rating?.criticRating?.toFloat(),
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier,
|
||||
)
|
||||
QuickDetails(details, null)
|
||||
series.genres?.mapNotNull { it.name }?.letNotEmpty {
|
||||
GenreText(it, Modifier.padding(bottom = padding))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import com.github.damontecres.wholphin.data.ChosenStreams
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.SeriesName
|
||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
|
|
@ -55,7 +55,7 @@ fun EpisodeDetailsHeader(
|
|||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
val padding = 8.dp
|
||||
EpisodeQuickDetails(dto)
|
||||
QuickDetails(ep.ui.quickDetails, ep.timeRemainingOrRuntime)
|
||||
|
||||
VideoStreamDetails(
|
||||
chosenStreams = chosenStreams,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -15,8 +18,10 @@ import com.github.damontecres.wholphin.services.NavigationManager
|
|||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
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.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -50,6 +55,7 @@ import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
|||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
|
@ -58,6 +64,7 @@ import kotlin.time.Duration
|
|||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toKotlinDuration
|
||||
|
||||
const val MAX_HOURS = 48L
|
||||
|
||||
|
|
@ -528,6 +535,7 @@ data class TvChannel(
|
|||
val imageUrl: String?,
|
||||
)
|
||||
|
||||
@Stable
|
||||
data class TvProgram(
|
||||
val id: UUID,
|
||||
val channelId: UUID,
|
||||
|
|
@ -548,6 +556,49 @@ data class TvProgram(
|
|||
) {
|
||||
val isFake = category == ProgramCategory.FAKE
|
||||
|
||||
val quickDetails by lazy {
|
||||
val now = LocalDateTime.now()
|
||||
buildAnnotatedString {
|
||||
val differentDay = start.toLocalDate() != now.toLocalDate()
|
||||
val time =
|
||||
DateUtils.formatDateRange(
|
||||
WholphinApplication.instance,
|
||||
start
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.epochSecond * 1000,
|
||||
end
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.epochSecond * 1000,
|
||||
DateUtils.FORMAT_SHOW_TIME or if (differentDay) DateUtils.FORMAT_SHOW_WEEKDAY else 0,
|
||||
)
|
||||
append(time)
|
||||
dot()
|
||||
|
||||
if (!isFake) {
|
||||
duration
|
||||
.roundMinutes
|
||||
.toString()
|
||||
.let(::append)
|
||||
dot()
|
||||
if (now.isAfter(start) && now.isBefore(end)) {
|
||||
java.time.Duration
|
||||
.between(now, end)
|
||||
.toKotlinDuration()
|
||||
.roundMinutes
|
||||
.let { append("$it left") }
|
||||
dot()
|
||||
}
|
||||
seasonEpisode?.let { "S${it.season} E${it.episode}" }?.let {
|
||||
append(it)
|
||||
dot()
|
||||
}
|
||||
officialRating?.let(::append)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NO_DATA = WholphinApplication.instance.getString(R.string.no_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -17,59 +14,15 @@ 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.components.DotSeparatedRow
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.StreamLabel
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import kotlin.time.toKotlinDuration
|
||||
|
||||
@Composable
|
||||
fun TvGuideHeader(
|
||||
program: TvProgram?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val now = LocalDateTime.now()
|
||||
val details =
|
||||
remember(program) {
|
||||
buildList {
|
||||
program?.let {
|
||||
val differentDay = it.start.toLocalDate() != now.toLocalDate()
|
||||
val time =
|
||||
DateUtils.formatDateRange(
|
||||
context,
|
||||
it.start
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.epochSecond * 1000,
|
||||
it.end
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toInstant()
|
||||
.epochSecond * 1000,
|
||||
DateUtils.FORMAT_SHOW_TIME or if (differentDay) DateUtils.FORMAT_SHOW_WEEKDAY else 0,
|
||||
)
|
||||
add(time)
|
||||
}
|
||||
if (program?.isFake == false) {
|
||||
program
|
||||
.duration
|
||||
.roundMinutes
|
||||
.toString()
|
||||
.let(::add)
|
||||
if (now.isAfter(program.start) && now.isBefore(program.end)) {
|
||||
java.time.Duration
|
||||
.between(now, program.end)
|
||||
.toKotlinDuration()
|
||||
.roundMinutes
|
||||
.let { add("$it left") }
|
||||
}
|
||||
program.seasonEpisode?.let { "S${it.season} E${it.episode}" }?.let(::add)
|
||||
program.officialRating?.let(::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
|
|
@ -100,13 +53,7 @@ fun TvGuideHeader(
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DotSeparatedRow(
|
||||
texts = details,
|
||||
communityRating = null,
|
||||
criticRating = null,
|
||||
textStyle = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier,
|
||||
)
|
||||
program?.quickDetails?.let { QuickDetails(it, null) }
|
||||
if (program?.isRepeat == true) {
|
||||
StreamLabel(stringResource(R.string.live_tv_repeat))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import com.github.damontecres.wholphin.data.ChosenStreams
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||
import com.github.damontecres.wholphin.ui.components.MovieQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
|
|
@ -65,7 +65,11 @@ fun MovieDetailsHeader(
|
|||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
val padding = 4.dp
|
||||
MovieQuickDetails(dto, Modifier.padding(bottom = padding))
|
||||
QuickDetails(
|
||||
movie.ui.quickDetails,
|
||||
movie.timeRemainingOrRuntime,
|
||||
Modifier.padding(bottom = padding),
|
||||
)
|
||||
|
||||
dto.genres?.letNotEmpty {
|
||||
GenreText(it, Modifier.padding(bottom = padding))
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import com.github.damontecres.wholphin.data.ChosenStreams
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||
|
||||
@Composable
|
||||
|
|
@ -33,7 +33,9 @@ fun FocusedEpisodeHeader(
|
|||
) {
|
||||
EpisodeName(dto, modifier = Modifier)
|
||||
|
||||
EpisodeQuickDetails(dto)
|
||||
ep?.ui?.quickDetails?.let {
|
||||
QuickDetails(it, ep.timeRemainingOrRuntime)
|
||||
}
|
||||
|
||||
if (dto != null) {
|
||||
VideoStreamDetails(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ import com.github.damontecres.wholphin.ui.components.GenreText
|
|||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.Optional
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.TrailerButton
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
|
|
@ -615,7 +615,7 @@ fun SeriesDetailsHeader(
|
|||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
SeriesQuickDetails(dto)
|
||||
QuickDetails(series.ui.quickDetails, null)
|
||||
dto.genres?.letNotEmpty {
|
||||
GenreText(it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
|
|||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.detail.discover.DiscoverQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.listToDotString
|
||||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -218,17 +218,30 @@ fun SeerrDiscoverPage(
|
|||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
val details =
|
||||
remember(focusedItem, ratingMap) {
|
||||
buildList {
|
||||
focusedItem
|
||||
?.releaseDate
|
||||
?.year
|
||||
?.toString()
|
||||
?.let(::add)
|
||||
}.let {
|
||||
val rating = focusedItem?.id?.let { ratingMap[it] }
|
||||
listToDotString(
|
||||
it,
|
||||
rating?.audienceRating,
|
||||
rating?.criticRating?.toFloat(),
|
||||
)
|
||||
}
|
||||
}
|
||||
HomePageHeader(
|
||||
title = focusedItem?.title,
|
||||
subtitle = focusedItem?.subtitle,
|
||||
overview = focusedItem?.overview,
|
||||
overviewTwoLines = true,
|
||||
quickDetails = {
|
||||
DiscoverQuickDetails(
|
||||
item = focusedItem,
|
||||
rating = focusedItem?.id?.let { ratingMap[it] },
|
||||
)
|
||||
},
|
||||
quickDetails = details,
|
||||
timeRemaining = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -55,11 +56,9 @@ import com.github.damontecres.wholphin.ui.components.CircularProgress
|
|||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.MovieQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
||||
|
|
@ -79,6 +78,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
|||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Composable
|
||||
fun HomePage(
|
||||
|
|
@ -451,13 +451,8 @@ fun HomePageHeader(
|
|||
subtitle = if (isEpisode) dto.name else null,
|
||||
overview = dto.overview,
|
||||
overviewTwoLines = isEpisode,
|
||||
quickDetails = {
|
||||
when (item.type) {
|
||||
BaseItemKind.EPISODE -> EpisodeQuickDetails(dto, Modifier)
|
||||
BaseItemKind.SERIES -> SeriesQuickDetails(dto, Modifier)
|
||||
else -> MovieQuickDetails(dto, Modifier)
|
||||
}
|
||||
},
|
||||
quickDetails = item.ui.quickDetails,
|
||||
timeRemaining = item.timeRemainingOrRuntime,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -469,7 +464,8 @@ fun HomePageHeader(
|
|||
subtitle: String?,
|
||||
overview: String?,
|
||||
overviewTwoLines: Boolean,
|
||||
quickDetails: (@Composable () -> Unit)?,
|
||||
quickDetails: AnnotatedString,
|
||||
timeRemaining: Duration?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -496,7 +492,7 @@ fun HomePageHeader(
|
|||
subtitle?.let {
|
||||
EpisodeName(it)
|
||||
}
|
||||
quickDetails?.invoke()
|
||||
QuickDetails(quickDetails, timeRemaining)
|
||||
val overviewModifier =
|
||||
Modifier
|
||||
.padding(0.dp)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.isActive
|
||||
import java.time.LocalDateTime
|
||||
|
||||
val LocalClock = compositionLocalOf<Clock> { throw IllegalStateException() }
|
||||
val LocalClock = compositionLocalOf<Clock> { Clock() }
|
||||
|
||||
/**
|
||||
* Represents the current time
|
||||
|
|
@ -21,21 +21,16 @@ data class Clock(
|
|||
/**
|
||||
* The current [LocalDateTime]
|
||||
*/
|
||||
val now: MutableState<LocalDateTime>,
|
||||
val now: MutableState<LocalDateTime> = mutableStateOf(LocalDateTime.now()),
|
||||
/**
|
||||
* The current time formatted as a string with [TimeFormatter]
|
||||
*/
|
||||
val timeString: MutableState<String>,
|
||||
val timeString: MutableState<String> = mutableStateOf(TimeFormatter.format(now.value)),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ProvideLocalClock(content: @Composable () -> Unit) {
|
||||
val clock =
|
||||
remember {
|
||||
LocalDateTime
|
||||
.now()
|
||||
.let { Clock(mutableStateOf(it), mutableStateOf(TimeFormatter.format(it))) }
|
||||
}
|
||||
val clock = remember { Clock() }
|
||||
LaunchedEffect(Unit) {
|
||||
while (isActive) {
|
||||
val now = LocalDateTime.now()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue