mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add main page header & backdrop
This commit is contained in:
parent
911bef1200
commit
54bdb25d49
10 changed files with 279 additions and 44 deletions
|
|
@ -14,6 +14,7 @@ import org.jellyfin.sdk.model.api.ImageType
|
||||||
data class BaseItem(
|
data class BaseItem(
|
||||||
val data: BaseItemDto,
|
val data: BaseItemDto,
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
|
val backdropImageUrl: String? = null,
|
||||||
) {
|
) {
|
||||||
@Transient val id = data.id
|
@Transient val id = data.id
|
||||||
|
|
||||||
|
|
@ -57,6 +58,23 @@ data class BaseItem(
|
||||||
fun from(
|
fun from(
|
||||||
dto: BaseItemDto,
|
dto: BaseItemDto,
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
) = BaseItem(dto, api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY))
|
): BaseItem {
|
||||||
|
val backdropImageUrl =
|
||||||
|
if (dto.type == BaseItemKind.EPISODE) {
|
||||||
|
val seriesId = dto.seriesId
|
||||||
|
if (seriesId != null) {
|
||||||
|
api.imageApi.getItemImageUrl(seriesId, ImageType.BACKDROP)
|
||||||
|
} else {
|
||||||
|
api.imageApi.getItemImageUrl(dto.id, ImageType.BACKDROP)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
api.imageApi.getItemImageUrl(dto.id, ImageType.BACKDROP)
|
||||||
|
}
|
||||||
|
return BaseItem(
|
||||||
|
dto,
|
||||||
|
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY),
|
||||||
|
backdropImageUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,14 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
fun CharSequence?.isNotNullOrBlank(): Boolean {
|
fun CharSequence?.isNotNullOrBlank(): Boolean {
|
||||||
|
|
@ -163,3 +168,16 @@ fun playOnClickSound(
|
||||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
audioManager.playSoundEffect(effectType)
|
audioManager.playSoundEffect(effectType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Duration.roundMinutes: Duration
|
||||||
|
get() = (this + 30.seconds).inWholeMinutes.minutes
|
||||||
|
|
||||||
|
val BaseItemDto.timeRemaining: Duration?
|
||||||
|
get() =
|
||||||
|
userData?.playbackPositionTicks?.let {
|
||||||
|
if (it > 0) {
|
||||||
|
runTimeTicks?.minus(it)?.ticks
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ val DefaultItemFields =
|
||||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||||
ItemFields.SEASON_USER_DATA,
|
ItemFields.SEASON_USER_DATA,
|
||||||
ItemFields.CHILD_COUNT,
|
ItemFields.CHILD_COUNT,
|
||||||
|
ItemFields.OVERVIEW,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Preview(
|
@Preview(
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ fun BannerCard(
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(4.dp),
|
.padding(4.dp),
|
||||||
) {
|
) {
|
||||||
if (played) {
|
if (played && (playPercent <= 0 || playPercent >= 100)) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.CheckCircle,
|
imageVector = Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ fun ItemCardImage(
|
||||||
fontFamily = FontAwesome,
|
fontFamily = FontAwesome,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (watched) {
|
if (watched && watchedPercent?.let { it <= 0 || it >= 100 } == true) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.CheckCircle,
|
imageVector = Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
|
@ -103,6 +105,7 @@ class SeriesViewModel
|
||||||
ItemFields.MEDIA_SOURCES,
|
ItemFields.MEDIA_SOURCES,
|
||||||
ItemFields.MEDIA_STREAMS,
|
ItemFields.MEDIA_STREAMS,
|
||||||
ItemFields.OVERVIEW,
|
ItemFields.OVERVIEW,
|
||||||
|
ItemFields.CUSTOM_RATING,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val pager = ItemPager(api, request, viewModelScope)
|
val pager = ItemPager(api, request, viewModelScope)
|
||||||
|
|
@ -111,6 +114,31 @@ class SeriesViewModel
|
||||||
episodes.value = pager
|
episodes.value = pager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setWatched(
|
||||||
|
itemId: UUID,
|
||||||
|
played: Boolean,
|
||||||
|
listIndex: Int,
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
if (played) {
|
||||||
|
api.playStateApi.markPlayedItem(itemId)
|
||||||
|
} else {
|
||||||
|
api.playStateApi.markUnplayedItem(itemId)
|
||||||
|
}
|
||||||
|
refreshEpisode(itemId, listIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshEpisode(
|
||||||
|
itemId: UUID,
|
||||||
|
listIndex: Int,
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
val base = api.userLibraryApi.getItem(itemId).content
|
||||||
|
val item = BaseItem.from(base, api)
|
||||||
|
episodes.value =
|
||||||
|
episodes.value!!.toMutableList().apply {
|
||||||
|
this[listIndex] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
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.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|
@ -28,6 +29,8 @@ import com.github.damontecres.dolphin.ui.components.StarRating
|
||||||
import com.github.damontecres.dolphin.ui.components.StarRatingPrecision
|
import com.github.damontecres.dolphin.ui.components.StarRatingPrecision
|
||||||
import com.github.damontecres.dolphin.ui.playOnClickSound
|
import com.github.damontecres.dolphin.ui.playOnClickSound
|
||||||
import com.github.damontecres.dolphin.ui.playSoundOnFocus
|
import com.github.damontecres.dolphin.ui.playSoundOnFocus
|
||||||
|
import com.github.damontecres.dolphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.dolphin.ui.timeRemaining
|
||||||
import com.github.damontecres.dolphin.util.formatDateTime
|
import com.github.damontecres.dolphin.util.formatDateTime
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
||||||
|
|
@ -57,10 +60,14 @@ fun FocusedEpisodeHeader(
|
||||||
add("S${dto.parentIndexNumber} E${dto.indexNumber}")
|
add("S${dto.parentIndexNumber} E${dto.indexNumber}")
|
||||||
}
|
}
|
||||||
dto.premiereDate?.let { add(formatDateTime(it)) }
|
dto.premiereDate?.let { add(formatDateTime(it)) }
|
||||||
dto.mediaSources?.firstOrNull()?.runTimeTicks?.ticks?.inWholeMinutes?.toString()?.let {
|
val duration = dto.runTimeTicks?.ticks
|
||||||
add(it)
|
duration
|
||||||
}
|
?.roundMinutes
|
||||||
dto.seriesStudio?.let { add(it) }
|
?.toString()
|
||||||
|
?.let {
|
||||||
|
add(it)
|
||||||
|
}
|
||||||
|
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
|
||||||
}
|
}
|
||||||
DotSeparatedRow(
|
DotSeparatedRow(
|
||||||
texts = details,
|
texts = details,
|
||||||
|
|
@ -71,33 +78,28 @@ fun FocusedEpisodeHeader(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
dto.criticRating?.let {
|
// TODO ratings?
|
||||||
Text(
|
dto.communityRating?.let {
|
||||||
text = "Critic: ${dto.criticRating}",
|
if (it > 0f) {
|
||||||
style = MaterialTheme.typography.bodySmall,
|
StarRating(
|
||||||
modifier = Modifier,
|
rating100 = (it * 10).toInt(),
|
||||||
)
|
onRatingChange = {},
|
||||||
|
enabled = false,
|
||||||
|
precision = StarRatingPrecision.HALF,
|
||||||
|
playSoundOnFocus = true,
|
||||||
|
modifier = Modifier.height(24.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
StarRating(
|
|
||||||
rating100 =
|
|
||||||
dto
|
|
||||||
.userData
|
|
||||||
?.rating
|
|
||||||
?.times(100)
|
|
||||||
?.toInt() ?: 0,
|
|
||||||
onRatingChange = {},
|
|
||||||
enabled = false,
|
|
||||||
precision = StarRatingPrecision.HALF,
|
|
||||||
playSoundOnFocus = true,
|
|
||||||
modifier = Modifier.height(32.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
dto.overview?.let { overview ->
|
dto.overview?.let { overview ->
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||||
val bgColor =
|
val bgColor =
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = .75f)
|
MaterialTheme.colorScheme.onPrimary.copy(alpha = .4f)
|
||||||
} else {
|
} else {
|
||||||
Color.Unspecified
|
Color.Unspecified
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,15 @@ fun SeriesOverview(
|
||||||
if (episodes.isNotEmpty()) {
|
if (episodes.isNotEmpty()) {
|
||||||
// TODO focus on first episode when changing seasons
|
// TODO focus on first episode when changing seasons
|
||||||
// firstItemFocusRequester.requestFocus()
|
// firstItemFocusRequester.requestFocus()
|
||||||
|
episodes.getOrNull(seasonEpisode.episode)?.let {
|
||||||
|
viewModel.refreshEpisode(it.id, seasonEpisode.episode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
}
|
||||||
|
|
||||||
if (series == null) {
|
if (series == null) {
|
||||||
// TODO
|
// TODO
|
||||||
Text(text = "Loading...")
|
Text(text = "Loading...")
|
||||||
|
|
@ -109,6 +115,11 @@ fun SeriesOverview(
|
||||||
},
|
},
|
||||||
watchOnClick = {
|
watchOnClick = {
|
||||||
// TODO toggle watched state
|
// TODO toggle watched state
|
||||||
|
episodes.getOrNull(seasonEpisode.episode)?.let {
|
||||||
|
val played = it.data.userData?.played ?: false
|
||||||
|
// TODO map indexes
|
||||||
|
viewModel.setWatched(it.id, !played, seasonEpisode.episode)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moreOnClick = {
|
moreOnClick = {
|
||||||
// TODO show more actions
|
// TODO show more actions
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Tab
|
import androidx.tv.material3.Tab
|
||||||
|
import androidx.tv.material3.TabDefaults
|
||||||
import androidx.tv.material3.TabRow
|
import androidx.tv.material3.TabRow
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
|
@ -133,13 +134,16 @@ fun SeriesOverviewContent(
|
||||||
selectedTabIndex = index
|
selectedTabIndex = index
|
||||||
onFocus.invoke(SeasonEpisode(index, 0))
|
onFocus.invoke(SeasonEpisode(index, 0))
|
||||||
},
|
},
|
||||||
|
colors =
|
||||||
|
TabDefaults.pillIndicatorTabColors(
|
||||||
|
// TODO
|
||||||
|
),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(focusRequesters[index]),
|
.focusRequester(focusRequesters[index]),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = season.name ?: "Season ${season.data.indexNumber}",
|
text = season.name ?: "Season ${season.data.indexNumber}",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,51 @@
|
||||||
package com.github.damontecres.dolphin.ui.main
|
package com.github.damontecres.dolphin.ui.main
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
||||||
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
||||||
|
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
||||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||||
|
import com.github.damontecres.dolphin.ui.roundMinutes
|
||||||
|
import com.github.damontecres.dolphin.ui.timeRemaining
|
||||||
|
import com.github.damontecres.dolphin.util.formatDateTime
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -32,9 +56,11 @@ import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -159,22 +185,149 @@ fun MainPage(
|
||||||
viewModel: MainViewModel = hiltViewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
||||||
Column(modifier = modifier) {
|
var focusedItem by remember { mutableStateOf<BaseItem?>(null) }
|
||||||
// TODO header?
|
Box(modifier = modifier) {
|
||||||
LazyColumn(
|
if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) {
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
val gradientColor = MaterialTheme.colorScheme.background
|
||||||
contentPadding = PaddingValues(16.dp),
|
AsyncImage(
|
||||||
) {
|
model = focusedItem?.backdropImageUrl,
|
||||||
items(homeRows) { row ->
|
contentDescription = null,
|
||||||
ItemRow(
|
contentScale = ContentScale.Fit,
|
||||||
title = stringResource(row.section.nameRes),
|
alignment = Alignment.TopEnd,
|
||||||
items = row.items,
|
modifier =
|
||||||
onClickItem = {
|
Modifier
|
||||||
navigationManager.navigateTo(it.destination())
|
.fillMaxHeight(.7f)
|
||||||
},
|
.fillMaxWidth(.7f)
|
||||||
onLongClickItem = {},
|
.alpha(.4f)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
.align(Alignment.TopEnd)
|
||||||
)
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(Color.Transparent, gradientColor),
|
||||||
|
startY = size.height * .33f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
colors = listOf(gradientColor, Color.Transparent),
|
||||||
|
startX = 0f,
|
||||||
|
endX = size.width * .5f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MainPageHeader(
|
||||||
|
item = focusedItem,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(.6f)
|
||||||
|
.fillMaxHeight(.33f)
|
||||||
|
.padding(16.dp),
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
contentPadding =
|
||||||
|
PaddingValues(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
top = 0.dp,
|
||||||
|
bottom = 48.dp,
|
||||||
|
),
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
items(homeRows) { row ->
|
||||||
|
ItemRow(
|
||||||
|
title = stringResource(row.section.nameRes),
|
||||||
|
items = row.items,
|
||||||
|
onClickItem = {
|
||||||
|
navigationManager.navigateTo(it.destination())
|
||||||
|
},
|
||||||
|
cardOnFocus = { isFocused, index ->
|
||||||
|
focusedItem = row.items.getOrNull(index)
|
||||||
|
},
|
||||||
|
onLongClickItem = {},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainPageHeader(
|
||||||
|
item: BaseItem?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
item?.let {
|
||||||
|
val dto = item.data
|
||||||
|
val isEpisode = item.type == BaseItemKind.EPISODE
|
||||||
|
val title = if (isEpisode) dto.seriesName ?: item.name else item.name
|
||||||
|
val subtitle = if (isEpisode) dto.name else null
|
||||||
|
val overview = dto.overview
|
||||||
|
val details =
|
||||||
|
buildList {
|
||||||
|
if (isEpisode) {
|
||||||
|
add("S${dto.parentIndexNumber} E${dto.indexNumber}")
|
||||||
|
}
|
||||||
|
if (isEpisode) {
|
||||||
|
dto.premiereDate?.let { add(formatDateTime(it)) }
|
||||||
|
} else {
|
||||||
|
dto.productionYear?.let { add(it.toString()) }
|
||||||
|
}
|
||||||
|
dto.runTimeTicks?.ticks?.roundMinutes?.let {
|
||||||
|
add(it.toString())
|
||||||
|
}
|
||||||
|
dto.timeRemaining?.roundMinutes?.let {
|
||||||
|
add("$it left")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title?.let {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
subtitle?.let {
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (details.isNotEmpty()) {
|
||||||
|
DotSeparatedRow(
|
||||||
|
texts = details,
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val overviewModifier =
|
||||||
|
Modifier
|
||||||
|
.padding(0.dp)
|
||||||
|
.height(48.dp)
|
||||||
|
if (overview.isNotNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = overview,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = overviewModifier,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(overviewModifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue