From 4d8d389582dd5211abf8f215132f09bd88da9a64 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Tue, 6 Jan 2026 16:52:44 -0500 Subject: [PATCH] Updates to discover tab --- .../ui/detail/CollectionFolderMovie.kt | 2 +- .../wholphin/ui/discover/DiscoverPage.kt | 12 +- .../wholphin/ui/discover/SeerrDiscoverPage.kt | 236 +++++++++++------- 3 files changed, 155 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index 1292cd5c..99dc1e7e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -61,7 +61,7 @@ fun CollectionFolderMovie( val focusRequester = remember { FocusRequester() } val firstTabFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } +// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } LaunchedEffect(selectedTabIndex) { logTab("movie", selectedTabIndex) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt index 117e4c43..9f5e54ee 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt @@ -59,7 +59,7 @@ fun DiscoverPage( var showHeader by rememberSaveable { mutableStateOf(true) } - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus("page") } Column( modifier = modifier, ) { @@ -83,14 +83,20 @@ fun DiscoverPage( 0 -> { SeerrDiscoverPage( preferences = preferences, - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize() + .focusRequester(focusRequester), ) } // Requests 1 -> { SeerrRequestsPage( - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize() + .focusRequester(focusRequester), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt index a1bfe126..acd5f380 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt @@ -1,30 +1,36 @@ package com.github.damontecres.wholphin.ui.discover +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.api.seerr.MediaApi import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.BackdropService @@ -32,14 +38,18 @@ import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard import com.github.damontecres.wholphin.ui.cards.ItemRow +import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.main.HomePageHeader import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberPosition -import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.tryRequestFocus +import com.github.damontecres.wholphin.util.DataLoadingState import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemKind import javax.inject.Inject @@ -48,35 +58,44 @@ import javax.inject.Inject class SeerrDiscoverViewModel @Inject constructor( + @param:ApplicationContext private val context: Context, private val seerrService: SeerrService, val navigationManager: NavigationManager, private val api: ApiClient, private val backdropService: BackdropService, ) : ViewModel() { - val recentlyAdded = MutableLiveData>(listOf()) - val discoverMovies = MutableLiveData>(listOf()) - val discoverTv = MutableLiveData>(listOf()) + val state = MutableStateFlow(DiscoverState()) init { viewModelScope.launchIO { backdropService.clearBackdrop() - val tv = - seerrService.api.mediaApi - .mediaGet( - take = 20, - filter = MediaApi.FilterMediaGet.ALLAVAILABLE, - sort = MediaApi.SortMediaGet.MEDIA_ADDED, - ).results - .orEmpty() -// Timber.v(tv.firstOrNull()?.jellyfinMediaId) } - viewModelScope.launchIO { - val movies = seerrService.discoverMovies() - discoverMovies.setValueOnMain(movies) + fetchAndUpdateState(seerrService::discoverMovies) { + this.copy(movies = DiscoverRowData(context.getString(R.string.movies), it)) } + fetchAndUpdateState(seerrService::discoverTv) { + this.copy(tv = DiscoverRowData(context.getString(R.string.tv_shows), it)) + } + } + + private fun fetchAndUpdateState( + getData: suspend () -> List, + copyFunc: DiscoverState.(DataLoadingState>) -> DiscoverState, + ) { viewModelScope.launchIO { - val tv = seerrService.discoverTv() - discoverTv.setValueOnMain(tv) + state.update { + copyFunc.invoke(it, DataLoadingState.Loading) + } + try { + val results = getData.invoke() + state.update { + copyFunc.invoke(it, DataLoadingState.Success(results)) + } + } catch (ex: Exception) { + state.update { + copyFunc.invoke(it, DataLoadingState.Error(ex)) + } + } } } @@ -89,32 +108,46 @@ class SeerrDiscoverViewModel } } +data class DiscoverRowData( + val title: String, + val items: DataLoadingState>, +) { + companion object { + val EMPTY = DiscoverRowData("", DataLoadingState.Pending) + } +} + +data class DiscoverState( + val movies: DiscoverRowData = DiscoverRowData.EMPTY, + val tv: DiscoverRowData = DiscoverRowData.EMPTY, +) + @Composable fun SeerrDiscoverPage( preferences: UserPreferences, modifier: Modifier = Modifier, viewModel: SeerrDiscoverViewModel = hiltViewModel(), ) { - val movies by viewModel.discoverMovies.observeAsState(listOf()) - val tv by viewModel.discoverTv.observeAsState(listOf()) + val state by viewModel.state.collectAsState() + val rows = listOf(state.movies, state.tv) - val focusRequester = remember { FocusRequester() } - LaunchedEffect(movies) { if (movies.isNotEmpty()) focusRequester.tryRequestFocus() } - val scrollState = rememberScrollState() - - var position by rememberPosition() - LaunchedEffect(position) { - position.let { - val item = if (it.row == 0) movies.getOrNull(it.column) else tv.getOrNull(it.column) - viewModel.updateBackdrop(item) - } - } + val focusRequesters = remember(2) { List(rows.size) { FocusRequester() } } + var position by rememberPosition(0, -1) val focusedItem = remember(position) { position.let { - if (it.row == 0) movies.getOrNull(it.column) else tv.getOrNull(it.column) + (rows.getOrNull(it.row)?.items as? DataLoadingState.Success)?.data?.getOrNull(it.column) } } + LaunchedEffect(focusedItem) { + viewModel.updateBackdrop(focusedItem) + } + var firstFocused by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(state.movies) { + if (!firstFocused && state.movies.items is DataLoadingState.Success<*>) { + firstFocused = focusRequesters.getOrNull(0)?.tryRequestFocus("discover") == true + } + } Column( modifier = modifier, @@ -129,33 +162,85 @@ fun SeerrDiscoverPage( }, modifier = Modifier - .padding(top = 48.dp, bottom = 32.dp, start = 32.dp) - .fillMaxHeight(.33f), + .fillMaxHeight(.33f) + .padding(top = 48.dp, bottom = 32.dp, start = 32.dp), ) - Column( + LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 40.dp), modifier = Modifier - .verticalScroll(scrollState) - .fillMaxSize() - .padding(start = 16.dp, end = 16.dp, bottom = 40.dp), + .focusRestorer() + .fillMaxSize(), ) { + itemsIndexed(rows) { rowIndex, row -> + DiscoverRow( + row = row, + onClickItem = { index, item -> + position = RowColumn(rowIndex, index) + if (item.jellyfinItemId != null) { + viewModel.navigationManager.navigateTo( + Destination.MediaItem( + itemId = item.jellyfinItemId, + type = BaseItemKind.MOVIE, + ), + ) + } else { + viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item)) + } + }, + onLongClickItem = { index, item -> }, + onCardFocus = { index -> position = RowColumn(rowIndex, index) }, + focusRequester = focusRequesters[rowIndex], + modifier = + Modifier + .fillMaxWidth(), + ) + } + } + } +} + +@Composable +fun DiscoverRow( + row: DiscoverRowData, + onClickItem: (Int, DiscoverItem) -> Unit, + onLongClickItem: (Int, DiscoverItem) -> Unit, + onCardFocus: (Int) -> Unit, + focusRequester: FocusRequester, + modifier: Modifier = Modifier, +) { + when (val state = row.items) { + is DataLoadingState.Error -> { + ErrorMessage(state.message, state.exception, modifier) + } + + DataLoadingState.Loading, + DataLoadingState.Pending, + -> { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + Text( + text = row.title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = stringResource(R.string.loading), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground, + ) + } + } + + is DataLoadingState.Success> -> { ItemRow( - title = stringResource(R.string.movies), - items = movies, - onClickItem = { index, item -> - if (item.jellyfinItemId != null) { - viewModel.navigationManager.navigateTo( - Destination.MediaItem( - itemId = item.jellyfinItemId, - type = BaseItemKind.MOVIE, - ), - ) - } else { - viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item)) - } - }, - onLongClickItem = { index, item -> }, + title = row.title, + items = state.data, + onClickItem = onClickItem, + onLongClickItem = onLongClickItem, cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit -> DiscoverItemCard( item = item, @@ -165,43 +250,12 @@ fun SeerrDiscoverPage( modifier = mod.onFocusChanged { if (it.isFocused) { - position = RowColumn(0, index) - } - }, - ) - }, - modifier = Modifier.focusRequester(focusRequester), - ) - ItemRow( - title = stringResource(R.string.tv_shows), - items = tv, - onClickItem = { index, item -> - if (item.jellyfinItemId != null) { - viewModel.navigationManager.navigateTo( - Destination.MediaItem( - itemId = item.jellyfinItemId, - type = BaseItemKind.SERIES, - ), - ) - } else { - viewModel.navigationManager.navigateTo(Destination.DiscoveredItem(item)) - } - }, - onLongClickItem = { index, item -> }, - cardContent = { index: Int, item: DiscoverItem?, mod: Modifier, onClick: () -> Unit, onLongClick: () -> Unit -> - DiscoverItemCard( - item = item, - onClick = onClick, - onLongClick = onLongClick, - showOverlay = false, - modifier = - mod.onFocusChanged { - if (it.isFocused) { - position = RowColumn(1, index) + onCardFocus.invoke(index) } }, ) }, + modifier = modifier.focusRequester(focusRequester), ) } }