Updates to discover tab

This commit is contained in:
Damontecres 2026-01-06 16:52:44 -05:00
parent f89eec622a
commit 4d8d389582
No known key found for this signature in database
3 changed files with 155 additions and 95 deletions

View file

@ -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)

View file

@ -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),
)
}

View file

@ -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<List<DiscoverItem>>(listOf())
val discoverMovies = MutableLiveData<List<DiscoverItem>>(listOf())
val discoverTv = MutableLiveData<List<DiscoverItem>>(listOf())
val state = MutableStateFlow<DiscoverState>(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<DiscoverItem>,
copyFunc: DiscoverState.(DataLoadingState<List<DiscoverItem>>) -> 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<List<DiscoverItem>>,
) {
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<List<DiscoverItem>> -> {
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),
)
}
}