Use typed HomeRow in HomeRowLoadingState.Success

This commit is contained in:
Damontecres 2026-01-23 18:18:54 -05:00
parent 71ddb59ac3
commit 2c6e7a57f0
No known key found for this signature in database
3 changed files with 176 additions and 79 deletions

View file

@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.HomeRow
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.Dispatchers
@ -61,7 +62,16 @@ abstract class RecommendedViewModel(
viewModelScope.launchIO {
val row = rows.value.getOrNull(position.row)
if (row is HomeRowLoadingState.Success) {
(row.items as? ApiRequestPager<*>)?.refreshItem(position.column, itemId)
when (row.data) {
is HomeRow.BaseItemHomeRow -> {
(row.data.items as? ApiRequestPager<*>)?.refreshItem(
position.column,
itemId,
)
}
is HomeRow.GenreHowRow -> {}
}
}
}
}

View file

@ -54,6 +54,7 @@ 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.ErrorMessage
import com.github.damontecres.wholphin.ui.components.Genre
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.QuickDetails
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
@ -69,6 +70,7 @@ import com.github.damontecres.wholphin.ui.playback.playable
import com.github.damontecres.wholphin.ui.playback.scale
import com.github.damontecres.wholphin.ui.rememberPosition
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.HomeRow
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.delay
@ -182,6 +184,13 @@ fun HomePage(
}
}
fun HomeRowLoadingState.getItem(index: Int) =
when (val data = (this as? HomeRowLoadingState.Success)?.data) {
is HomeRow.BaseItemHomeRow -> data.items.getOrNull(index)
is HomeRow.GenreHowRow -> data.items.getOrNull(index)
null -> null
}
@Composable
fun HomePageContent(
homeRows: List<HomeRowLoadingState>,
@ -198,7 +207,7 @@ fun HomePageContent(
var position by rememberPosition()
val focusedItem =
position.let {
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
homeRows.getOrNull(it.row)?.getItem(it.column)
}
val rowFocusRequesters = remember(homeRows) { List(homeRows.size) { FocusRequester() } }
@ -211,7 +220,7 @@ fun HomePageContent(
} else {
// Waiting for the first home row to load, then focus on it
homeRows
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
.indexOfFirst { it is HomeRowLoadingState.Success && it.data.isNotEmpty() }
.takeIf { it >= 0 }
?.let {
rowFocusRequesters[it].tryRequestFocus()
@ -228,17 +237,29 @@ fun HomePageContent(
}
}
LaunchedEffect(onUpdateBackdrop, focusedItem) {
focusedItem?.let { onUpdateBackdrop.invoke(it) }
(focusedItem as? BaseItem)?.let { onUpdateBackdrop.invoke(it) }
}
Box(modifier = modifier) {
Column(modifier = Modifier.fillMaxSize()) {
HomePageHeader(
item = focusedItem,
modifier =
Modifier
.padding(top = 48.dp, bottom = 32.dp, start = 32.dp)
.fillMaxHeight(.33f),
)
val headerModifier =
Modifier
.padding(top = 48.dp, bottom = 32.dp, start = 32.dp)
.fillMaxHeight(.33f)
when (focusedItem) {
is BaseItem -> {
HomePageHeader(
item = focusedItem,
modifier = headerModifier,
)
}
is Genre -> {
Text(
text = focusedItem.name,
modifier = headerModifier,
)
}
}
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp),
@ -307,73 +328,55 @@ fun HomePageContent(
}
is HomeRowLoadingState.Success -> {
if (row.items.isNotEmpty()) {
val viewOptions = row.viewOptions
ItemRow(
title = row.title,
items = row.items,
onClickItem = { index, item ->
onClickItem.invoke(RowColumn(rowIndex, index), item)
},
onLongClickItem = { index, item ->
onLongClickItem.invoke(RowColumn(rowIndex, index), item)
},
modifier =
Modifier
.fillMaxWidth()
.focusGroup()
.focusRequester(rowFocusRequesters[rowIndex])
.animateItem(),
horizontalPadding = viewOptions.spacing.dp,
cardContent = { index, item, cardModifier, onClick, onLongClick ->
BannerCard(
name = item?.data?.seriesName ?: item?.name,
item = item,
aspectRatio = viewOptions.aspectRatio.ratio,
imageType = viewOptions.imageType.imageType,
imageContentScale = viewOptions.contentScale.scale,
cornerText = item?.ui?.episdodeUnplayedCornerText,
played = item?.data?.userData?.played ?: false,
favorite = item?.favorite ?: false,
playPercent =
item?.data?.userData?.playedPercentage
?: 0.0,
onClick = onClick,
onLongClick = onLongClick,
modifier =
cardModifier
.onFocusChanged {
if (it.isFocused) {
position = RowColumn(rowIndex, index)
// item?.let(onUpdateBackdrop)
}
if (it.isFocused && onFocusPosition != null) {
val nonEmptyRowBefore =
homeRows
.subList(0, rowIndex)
.count {
it is HomeRowLoadingState.Success && it.items.isEmpty()
}
onFocusPosition.invoke(
RowColumn(
rowIndex - nonEmptyRowBefore,
index,
),
)
}
}.onKeyEvent {
if (isPlayKeyUp(it) && item?.type?.playable == true) {
Timber.v("Clicked play on ${item.id}")
onClickPlay.invoke(position, item)
return@onKeyEvent true
}
return@onKeyEvent false
},
interactionSource = null,
cardHeight = viewOptions.heightDp.dp,
if (row.data.isNotEmpty()) {
when (row.data) {
is HomeRow.BaseItemHomeRow -> {
BaseItemRow(
row = row,
rowData = row.data,
onClickItem = { index, item ->
onClickItem.invoke(RowColumn(rowIndex, index), item)
},
onLongClickItem = { index, item ->
onLongClickItem.invoke(
RowColumn(rowIndex, index),
item,
)
},
onCardFocus = { index ->
position = RowColumn(rowIndex, index)
if (onFocusPosition != null) {
val nonEmptyRowBefore =
homeRows
.subList(0, rowIndex)
.count {
it is HomeRowLoadingState.Success && it.data.isEmpty()
}
onFocusPosition.invoke(
RowColumn(
rowIndex - nonEmptyRowBefore,
index,
),
)
}
},
onClickPlay = { index ->
row.data.items.get(index)?.let {
onClickPlay.invoke(
RowColumn(rowIndex, index),
it,
)
}
},
modifier = Modifier,
focusRequester = rowFocusRequesters[rowIndex],
)
},
)
}
is HomeRow.GenreHowRow -> {
TODO()
}
}
}
}
}
@ -475,3 +478,62 @@ fun HomePageHeader(
}
}
}
@Composable
fun BaseItemRow(
row: HomeRowLoadingState.Success,
rowData: HomeRow.BaseItemHomeRow,
onClickItem: (Int, BaseItem) -> Unit,
onLongClickItem: (Int, BaseItem) -> Unit,
onClickPlay: (Int) -> Unit,
onCardFocus: (Int) -> Unit,
modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() },
) {
val viewOptions = row.viewOptions
ItemRow(
title = row.title,
items = rowData.items,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
modifier =
modifier
.fillMaxWidth()
.focusGroup()
.focusRequester(focusRequester),
horizontalPadding = viewOptions.spacing.dp,
cardContent = { index, item, cardModifier, onClick, onLongClick ->
BannerCard(
name = item?.data?.seriesName ?: item?.name,
item = item,
aspectRatio = viewOptions.aspectRatio.ratio,
imageType = viewOptions.imageType.imageType,
imageContentScale = viewOptions.contentScale.scale,
cornerText = item?.ui?.episdodeUnplayedCornerText,
played = item?.data?.userData?.played ?: false,
favorite = item?.favorite ?: false,
playPercent =
item?.data?.userData?.playedPercentage
?: 0.0,
onClick = onClick,
onLongClick = onLongClick,
modifier =
cardModifier
.onFocusChanged {
if (it.isFocused) {
onCardFocus.invoke(index)
}
}.onKeyEvent {
if (isPlayKeyUp(it) && item?.type?.playable == true) {
Timber.v("Clicked play on ${item.id}")
onClickPlay.invoke(index)
return@onKeyEvent true
}
return@onKeyEvent false
},
interactionSource = null,
cardHeight = viewOptions.heightDp.dp,
)
},
)
}

View file

@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.util
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.ui.components.Genre
/**
* Generic state for loading something from the API
@ -59,9 +60,15 @@ sealed interface HomeRowLoadingState {
data class Success(
override val title: String,
val items: List<BaseItem?>,
val viewOptions: HomeRowViewOptions = HomeRowViewOptions(),
) : HomeRowLoadingState
val data: HomeRow,
) : HomeRowLoadingState {
constructor(
title: String,
items: List<BaseItem?>,
viewOptions: HomeRowViewOptions = HomeRowViewOptions(),
) : this(title, viewOptions, HomeRow.BaseItemHomeRow(items))
}
data class Error(
override val title: String,
@ -73,6 +80,24 @@ sealed interface HomeRowLoadingState {
}
}
sealed interface HomeRow {
fun isEmpty(): Boolean
fun isNotEmpty(): Boolean = !isEmpty()
data class BaseItemHomeRow(
val items: List<BaseItem?>,
) : HomeRow {
override fun isEmpty(): Boolean = items.isEmpty()
}
data class GenreHowRow(
val items: List<Genre>,
) : HomeRow {
override fun isEmpty(): Boolean = items.isEmpty()
}
}
/**
* Generic state for loading something from the API
*