mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Updates to discover tab
This commit is contained in:
parent
f89eec622a
commit
4d8d389582
3 changed files with 155 additions and 95 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue