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.main.HomePageContent
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.ApiRequestPager 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.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -61,7 +62,16 @@ abstract class RecommendedViewModel(
viewModelScope.launchIO { viewModelScope.launchIO {
val row = rows.value.getOrNull(position.row) val row = rows.value.getOrNull(position.row)
if (row is HomeRowLoadingState.Success) { 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.DialogPopup
import com.github.damontecres.wholphin.ui.components.EpisodeName import com.github.damontecres.wholphin.ui.components.EpisodeName
import com.github.damontecres.wholphin.ui.components.ErrorMessage 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.LoadingPage
import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.components.QuickDetails
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel 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.playback.scale
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
import com.github.damontecres.wholphin.util.HomeRow
import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.delay 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 @Composable
fun HomePageContent( fun HomePageContent(
homeRows: List<HomeRowLoadingState>, homeRows: List<HomeRowLoadingState>,
@ -198,7 +207,7 @@ fun HomePageContent(
var position by rememberPosition() var position by rememberPosition()
val focusedItem = val focusedItem =
position.let { 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() } } val rowFocusRequesters = remember(homeRows) { List(homeRows.size) { FocusRequester() } }
@ -211,7 +220,7 @@ fun HomePageContent(
} else { } else {
// Waiting for the first home row to load, then focus on it // Waiting for the first home row to load, then focus on it
homeRows homeRows
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() } .indexOfFirst { it is HomeRowLoadingState.Success && it.data.isNotEmpty() }
.takeIf { it >= 0 } .takeIf { it >= 0 }
?.let { ?.let {
rowFocusRequesters[it].tryRequestFocus() rowFocusRequesters[it].tryRequestFocus()
@ -228,17 +237,29 @@ fun HomePageContent(
} }
} }
LaunchedEffect(onUpdateBackdrop, focusedItem) { LaunchedEffect(onUpdateBackdrop, focusedItem) {
focusedItem?.let { onUpdateBackdrop.invoke(it) } (focusedItem as? BaseItem)?.let { onUpdateBackdrop.invoke(it) }
} }
Box(modifier = modifier) { Box(modifier = modifier) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
HomePageHeader( val headerModifier =
item = focusedItem, Modifier
modifier = .padding(top = 48.dp, bottom = 32.dp, start = 32.dp)
Modifier .fillMaxHeight(.33f)
.padding(top = 48.dp, bottom = 32.dp, start = 32.dp) when (focusedItem) {
.fillMaxHeight(.33f), is BaseItem -> {
) HomePageHeader(
item = focusedItem,
modifier = headerModifier,
)
}
is Genre -> {
Text(
text = focusedItem.name,
modifier = headerModifier,
)
}
}
LazyColumn( LazyColumn(
state = listState, state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@ -307,73 +328,55 @@ fun HomePageContent(
} }
is HomeRowLoadingState.Success -> { is HomeRowLoadingState.Success -> {
if (row.items.isNotEmpty()) { if (row.data.isNotEmpty()) {
val viewOptions = row.viewOptions when (row.data) {
ItemRow( is HomeRow.BaseItemHomeRow -> {
title = row.title, BaseItemRow(
items = row.items, row = row,
onClickItem = { index, item -> rowData = row.data,
onClickItem.invoke(RowColumn(rowIndex, index), item) onClickItem = { index, item ->
}, onClickItem.invoke(RowColumn(rowIndex, index), item)
onLongClickItem = { index, item -> },
onLongClickItem.invoke(RowColumn(rowIndex, index), item) onLongClickItem = { index, item ->
}, onLongClickItem.invoke(
modifier = RowColumn(rowIndex, index),
Modifier item,
.fillMaxWidth() )
.focusGroup() },
.focusRequester(rowFocusRequesters[rowIndex]) onCardFocus = { index ->
.animateItem(), position = RowColumn(rowIndex, index)
horizontalPadding = viewOptions.spacing.dp, if (onFocusPosition != null) {
cardContent = { index, item, cardModifier, onClick, onLongClick -> val nonEmptyRowBefore =
BannerCard( homeRows
name = item?.data?.seriesName ?: item?.name, .subList(0, rowIndex)
item = item, .count {
aspectRatio = viewOptions.aspectRatio.ratio, it is HomeRowLoadingState.Success && it.data.isEmpty()
imageType = viewOptions.imageType.imageType, }
imageContentScale = viewOptions.contentScale.scale, onFocusPosition.invoke(
cornerText = item?.ui?.episdodeUnplayedCornerText, RowColumn(
played = item?.data?.userData?.played ?: false, rowIndex - nonEmptyRowBefore,
favorite = item?.favorite ?: false, index,
playPercent = ),
item?.data?.userData?.playedPercentage )
?: 0.0, }
onClick = onClick, },
onLongClick = onLongClick, onClickPlay = { index ->
modifier = row.data.items.get(index)?.let {
cardModifier onClickPlay.invoke(
.onFocusChanged { RowColumn(rowIndex, index),
if (it.isFocused) { it,
position = RowColumn(rowIndex, index) )
// item?.let(onUpdateBackdrop) }
} },
if (it.isFocused && onFocusPosition != null) { modifier = Modifier,
val nonEmptyRowBefore = focusRequester = rowFocusRequesters[rowIndex],
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,
) )
}, }
)
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.BaseItem
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.ui.components.Genre
/** /**
* Generic state for loading something from the API * Generic state for loading something from the API
@ -59,9 +60,15 @@ sealed interface HomeRowLoadingState {
data class Success( data class Success(
override val title: String, override val title: String,
val items: List<BaseItem?>,
val viewOptions: HomeRowViewOptions = HomeRowViewOptions(), 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( data class Error(
override val title: String, 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 * Generic state for loading something from the API
* *