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
|
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.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.CardGridItem
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
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.formatDateTime
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playback.playable
|
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.seasonEpisode
|
||||||
import com.github.damontecres.wholphin.ui.seasonEpisodePadded
|
import com.github.damontecres.wholphin.ui.seasonEpisodePadded
|
||||||
import com.github.damontecres.wholphin.ui.seriesProductionYears
|
import com.github.damontecres.wholphin.ui.seriesProductionYears
|
||||||
|
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -73,6 +82,61 @@ data class BaseItem(
|
||||||
|
|
||||||
val favorite get() = data.userData?.isFavorite ?: false
|
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? =
|
private fun dateAsIndex(): Int? =
|
||||||
data.premiereDate
|
data.premiereDate
|
||||||
?.let {
|
?.let {
|
||||||
|
|
@ -124,3 +188,8 @@ data class BaseItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
val BaseItemDto.aspectRatioFloat: Float? get() = width?.let { w -> height?.let { h -> w.toFloat() / h.toFloat() } }
|
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
|
package com.github.damontecres.wholphin.ui
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
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 com.github.damontecres.wholphin.R
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentType
|
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||||
|
|
@ -152,3 +155,31 @@ val MediaSegmentType.skipStringRes: Int
|
||||||
MediaSegmentType.OUTRO -> R.string.skip_segment_outro
|
MediaSegmentType.OUTRO -> R.string.skip_segment_outro
|
||||||
MediaSegmentType.INTRO -> R.string.skip_segment_intro
|
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
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
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.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SeriesName(
|
fun SeriesName(
|
||||||
|
|
@ -55,52 +46,3 @@ fun EpisodeName(
|
||||||
episode: BaseItemDto?,
|
episode: BaseItemDto?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) = EpisodeName(episode?.episodeTitle ?: episode?.name, 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.api.seerr.model.MovieDetails
|
||||||
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
import com.github.damontecres.wholphin.data.model.DiscoverRating
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
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.GenreText
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
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.letNotEmpty
|
||||||
|
import com.github.damontecres.wholphin.ui.listToDotString
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -88,16 +89,16 @@ fun DiscoverMovieDetailsHeader(
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.certification
|
?.certification
|
||||||
?.let(::add)
|
?.let(::add)
|
||||||
|
}.let {
|
||||||
|
listToDotString(
|
||||||
|
it,
|
||||||
|
rating?.audienceRating,
|
||||||
|
rating?.criticRating?.toFloat(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DotSeparatedRow(
|
QuickDetails(details, null)
|
||||||
texts = details,
|
|
||||||
communityRating = rating?.audienceRating,
|
|
||||||
criticRating = rating?.criticRating?.toFloat(),
|
|
||||||
textStyle = MaterialTheme.typography.titleSmall,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
movie.genres?.mapNotNull { it.name }?.letNotEmpty {
|
movie.genres?.mapNotNull { it.name }?.letNotEmpty {
|
||||||
GenreText(it, Modifier.padding(bottom = padding))
|
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.DialogItem
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
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.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.GenreText
|
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
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.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
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.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
|
|
@ -478,16 +479,16 @@ fun DiscoverSeriesDetailsHeader(
|
||||||
?.toString()
|
?.toString()
|
||||||
?.let(::add)
|
?.let(::add)
|
||||||
// TODO
|
// TODO
|
||||||
|
}.let {
|
||||||
|
listToDotString(
|
||||||
|
it,
|
||||||
|
rating?.audienceRating,
|
||||||
|
rating?.criticRating?.toFloat(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DotSeparatedRow(
|
QuickDetails(details, null)
|
||||||
texts = details,
|
|
||||||
communityRating = rating?.audienceRating,
|
|
||||||
criticRating = rating?.criticRating?.toFloat(),
|
|
||||||
textStyle = MaterialTheme.typography.titleSmall,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
series.genres?.mapNotNull { it.name }?.letNotEmpty {
|
series.genres?.mapNotNull { it.name }?.letNotEmpty {
|
||||||
GenreText(it, Modifier.padding(bottom = padding))
|
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.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
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.OverviewText
|
||||||
|
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.SeriesName
|
import com.github.damontecres.wholphin.ui.components.SeriesName
|
||||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
|
@ -55,7 +55,7 @@ fun EpisodeDetailsHeader(
|
||||||
modifier = Modifier.fillMaxWidth(.60f),
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
val padding = 8.dp
|
val padding = 8.dp
|
||||||
EpisodeQuickDetails(dto)
|
QuickDetails(ep.ui.quickDetails, ep.timeRemainingOrRuntime)
|
||||||
|
|
||||||
VideoStreamDetails(
|
VideoStreamDetails(
|
||||||
chosenStreams = chosenStreams,
|
chosenStreams = chosenStreams,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.livetv
|
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
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.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
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.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
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.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
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 org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -58,6 +64,7 @@ import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.toKotlinDuration
|
||||||
|
|
||||||
const val MAX_HOURS = 48L
|
const val MAX_HOURS = 48L
|
||||||
|
|
||||||
|
|
@ -528,6 +535,7 @@ data class TvChannel(
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
data class TvProgram(
|
data class TvProgram(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val channelId: UUID,
|
val channelId: UUID,
|
||||||
|
|
@ -548,6 +556,49 @@ data class TvProgram(
|
||||||
) {
|
) {
|
||||||
val isFake = category == ProgramCategory.FAKE
|
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 {
|
companion object {
|
||||||
private val NO_DATA = WholphinApplication.instance.getString(R.string.no_data)
|
private val NO_DATA = WholphinApplication.instance.getString(R.string.no_data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail.livetv
|
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.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.ui.components.DotSeparatedRow
|
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
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.components.StreamLabel
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneId
|
|
||||||
import kotlin.time.toKotlinDuration
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvGuideHeader(
|
fun TvGuideHeader(
|
||||||
program: TvProgram?,
|
program: TvProgram?,
|
||||||
modifier: Modifier = Modifier,
|
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(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
|
|
@ -100,13 +53,7 @@ fun TvGuideHeader(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
DotSeparatedRow(
|
program?.quickDetails?.let { QuickDetails(it, null) }
|
||||||
texts = details,
|
|
||||||
communityRating = null,
|
|
||||||
criticRating = null,
|
|
||||||
textStyle = MaterialTheme.typography.titleSmall,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
if (program?.isRepeat == true) {
|
if (program?.isRepeat == true) {
|
||||||
StreamLabel(stringResource(R.string.live_tv_repeat))
|
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.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.GenreText
|
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.OverviewText
|
||||||
|
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
|
|
@ -65,7 +65,11 @@ fun MovieDetailsHeader(
|
||||||
modifier = Modifier.fillMaxWidth(.60f),
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
val padding = 4.dp
|
val padding = 4.dp
|
||||||
MovieQuickDetails(dto, Modifier.padding(bottom = padding))
|
QuickDetails(
|
||||||
|
movie.ui.quickDetails,
|
||||||
|
movie.timeRemainingOrRuntime,
|
||||||
|
Modifier.padding(bottom = padding),
|
||||||
|
)
|
||||||
|
|
||||||
dto.genres?.letNotEmpty {
|
dto.genres?.letNotEmpty {
|
||||||
GenreText(it, Modifier.padding(bottom = padding))
|
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.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
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.OverviewText
|
||||||
|
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
import com.github.damontecres.wholphin.ui.components.VideoStreamDetails
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -33,7 +33,9 @@ fun FocusedEpisodeHeader(
|
||||||
) {
|
) {
|
||||||
EpisodeName(dto, modifier = Modifier)
|
EpisodeName(dto, modifier = Modifier)
|
||||||
|
|
||||||
EpisodeQuickDetails(dto)
|
ep?.ui?.quickDetails?.let {
|
||||||
|
QuickDetails(it, ep.timeRemainingOrRuntime)
|
||||||
|
}
|
||||||
|
|
||||||
if (dto != null) {
|
if (dto != null) {
|
||||||
VideoStreamDetails(
|
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.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.Optional
|
import com.github.damontecres.wholphin.ui.components.Optional
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
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.components.TrailerButton
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
|
|
@ -615,7 +615,7 @@ fun SeriesDetailsHeader(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.fillMaxWidth(.60f),
|
modifier = Modifier.fillMaxWidth(.60f),
|
||||||
) {
|
) {
|
||||||
SeriesQuickDetails(dto)
|
QuickDetails(series.ui.quickDetails, null)
|
||||||
dto.genres?.letNotEmpty {
|
dto.genres?.letNotEmpty {
|
||||||
GenreText(it)
|
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.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
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.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.listToDotString
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -218,17 +218,30 @@ fun SeerrDiscoverPage(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
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(
|
HomePageHeader(
|
||||||
title = focusedItem?.title,
|
title = focusedItem?.title,
|
||||||
subtitle = focusedItem?.subtitle,
|
subtitle = focusedItem?.subtitle,
|
||||||
overview = focusedItem?.overview,
|
overview = focusedItem?.overview,
|
||||||
overviewTwoLines = true,
|
overviewTwoLines = true,
|
||||||
quickDetails = {
|
quickDetails = details,
|
||||||
DiscoverQuickDetails(
|
timeRemaining = null,
|
||||||
item = focusedItem,
|
|
||||||
rating = focusedItem?.id?.let { ratingMap[it] },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
.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.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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
|
||||||
|
|
@ -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.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
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.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.MovieQuickDetails
|
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||||
import com.github.damontecres.wholphin.ui.components.SeriesQuickDetails
|
|
||||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumnSaver
|
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 org.jellyfin.sdk.model.api.MediaType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage(
|
fun HomePage(
|
||||||
|
|
@ -451,13 +451,8 @@ fun HomePageHeader(
|
||||||
subtitle = if (isEpisode) dto.name else null,
|
subtitle = if (isEpisode) dto.name else null,
|
||||||
overview = dto.overview,
|
overview = dto.overview,
|
||||||
overviewTwoLines = isEpisode,
|
overviewTwoLines = isEpisode,
|
||||||
quickDetails = {
|
quickDetails = item.ui.quickDetails,
|
||||||
when (item.type) {
|
timeRemaining = item.timeRemainingOrRuntime,
|
||||||
BaseItemKind.EPISODE -> EpisodeQuickDetails(dto, Modifier)
|
|
||||||
BaseItemKind.SERIES -> SeriesQuickDetails(dto, Modifier)
|
|
||||||
else -> MovieQuickDetails(dto, Modifier)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -469,7 +464,8 @@ fun HomePageHeader(
|
||||||
subtitle: String?,
|
subtitle: String?,
|
||||||
overview: String?,
|
overview: String?,
|
||||||
overviewTwoLines: Boolean,
|
overviewTwoLines: Boolean,
|
||||||
quickDetails: (@Composable () -> Unit)?,
|
quickDetails: AnnotatedString,
|
||||||
|
timeRemaining: Duration?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -496,7 +492,7 @@ fun HomePageHeader(
|
||||||
subtitle?.let {
|
subtitle?.let {
|
||||||
EpisodeName(it)
|
EpisodeName(it)
|
||||||
}
|
}
|
||||||
quickDetails?.invoke()
|
QuickDetails(quickDetails, timeRemaining)
|
||||||
val overviewModifier =
|
val overviewModifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(0.dp)
|
.padding(0.dp)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
val LocalClock = compositionLocalOf<Clock> { throw IllegalStateException() }
|
val LocalClock = compositionLocalOf<Clock> { Clock() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the current time
|
* Represents the current time
|
||||||
|
|
@ -21,21 +21,16 @@ data class Clock(
|
||||||
/**
|
/**
|
||||||
* The current [LocalDateTime]
|
* The current [LocalDateTime]
|
||||||
*/
|
*/
|
||||||
val now: MutableState<LocalDateTime>,
|
val now: MutableState<LocalDateTime> = mutableStateOf(LocalDateTime.now()),
|
||||||
/**
|
/**
|
||||||
* The current time formatted as a string with [TimeFormatter]
|
* The current time formatted as a string with [TimeFormatter]
|
||||||
*/
|
*/
|
||||||
val timeString: MutableState<String>,
|
val timeString: MutableState<String> = mutableStateOf(TimeFormatter.format(now.value)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProvideLocalClock(content: @Composable () -> Unit) {
|
fun ProvideLocalClock(content: @Composable () -> Unit) {
|
||||||
val clock =
|
val clock = remember { Clock() }
|
||||||
remember {
|
|
||||||
LocalDateTime
|
|
||||||
.now()
|
|
||||||
.let { Clock(mutableStateOf(it), mutableStateOf(TimeFormatter.format(it))) }
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue