diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index 869a454d..44dffeec 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -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 -> {} + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 88b2fd27..092b2f80 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -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, @@ -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, + ) + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt index 50e42889..49f9be61 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt @@ -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, val viewOptions: HomeRowViewOptions = HomeRowViewOptions(), - ) : HomeRowLoadingState + val data: HomeRow, + ) : HomeRowLoadingState { + constructor( + title: String, + items: List, + 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, + ) : HomeRow { + override fun isEmpty(): Boolean = items.isEmpty() + } + + data class GenreHowRow( + val items: List, + ) : HomeRow { + override fun isEmpty(): Boolean = items.isEmpty() + } +} + /** * Generic state for loading something from the API *