mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Load movie & tv recommendations asynchronously (#183)
Previously all of the rows on the recommended tab for Movies & TV Shows were loaded sequentially before any results were shown. This is not a great experience because some of the queries (eg top rated unwatched) can take a while sometimes. This PR starts displaying content once the first 1 or 2 rows is loaded and then shows placeholders while the rest are querying. Finally the row queries are now mostly independent so if one errors out, the rest of the rows will still be displayed. Closes #180
This commit is contained in:
parent
0fb1a69556
commit
98fc996bd5
6 changed files with 422 additions and 179 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -15,16 +17,23 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
|||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||
import com.github.damontecres.wholphin.ui.main.HomeRow
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -34,33 +43,73 @@ import org.jellyfin.sdk.model.api.SortOrder
|
|||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@HiltViewModel(assistedFactory = RecommendedMovieViewModel.Factory::class)
|
||||
class RecommendedMovieViewModel
|
||||
@Inject
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
@Assisted val parentId: UUID,
|
||||
) : ViewModel() {
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val rows = MutableLiveData<List<HomeRow>>()
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(parentId: UUID): RecommendedMovieViewModel
|
||||
}
|
||||
|
||||
fun init(
|
||||
preferences: UserPreferences,
|
||||
parentId: UUID,
|
||||
) {
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
|
||||
val rows =
|
||||
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
||||
rowTitles
|
||||
.map {
|
||||
HomeRowLoadingState.Pending(
|
||||
context.getString(it),
|
||||
)
|
||||
}.toMutableList(),
|
||||
)
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
enableUserData = true,
|
||||
try {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
enableUserData = true,
|
||||
)
|
||||
val resumeItems =
|
||||
ApiRequestPager(
|
||||
api,
|
||||
resumeItemsRequest,
|
||||
GetResumeItemsRequestHandler,
|
||||
viewModelScope,
|
||||
).init()
|
||||
if (resumeItems.isNotEmpty()) {
|
||||
resumeItems.getBlocking(0)
|
||||
}
|
||||
|
||||
update(
|
||||
0,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.continue_watching),
|
||||
resumeItems,
|
||||
),
|
||||
)
|
||||
val resumeItems =
|
||||
ApiRequestPager(api, resumeItemsRequest, GetResumeItemsRequestHandler, viewModelScope)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
loading.value = LoadingState.Success
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception fetching movie recommendations")
|
||||
withContext(Dispatchers.Main) {
|
||||
loading.value = LoadingState.Error(ex)
|
||||
}
|
||||
}
|
||||
|
||||
val recentlyReleasedRequest =
|
||||
GetItemsRequest(
|
||||
|
|
@ -109,22 +158,50 @@ class RecommendedMovieViewModel
|
|||
val unwatchedTopRatedItems =
|
||||
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
|
||||
val rows = listOf(resumeItems, recentlyReleasedItems, recentlyAddedItems, suggestedItems, unwatchedTopRatedItems)
|
||||
rows.forEach { it.init() }
|
||||
val homeRows =
|
||||
val rows =
|
||||
listOf(
|
||||
HomeRow(R.string.continue_watching, resumeItems),
|
||||
HomeRow(R.string.recently_released, recentlyReleasedItems),
|
||||
HomeRow(R.string.recently_added, recentlyAddedItems),
|
||||
HomeRow(R.string.suggestions, suggestedItems),
|
||||
HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
|
||||
).filter { it.items.isNotEmpty() }
|
||||
withContext(Dispatchers.Main) {
|
||||
this@RecommendedMovieViewModel.rows.value = homeRows
|
||||
loading.value = LoadingState.Success
|
||||
R.string.recently_released to recentlyReleasedItems,
|
||||
R.string.recently_added to recentlyAddedItems,
|
||||
R.string.suggestions to suggestedItems,
|
||||
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||
)
|
||||
|
||||
rows.forEachIndexed { index, (title, pager) ->
|
||||
viewModelScope.launchIO {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
}
|
||||
update(index + 1, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(
|
||||
position: Int,
|
||||
row: HomeRowLoadingState,
|
||||
) {
|
||||
rows.update { current ->
|
||||
current.apply { set(position, row) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val rowTitles =
|
||||
listOf(
|
||||
R.string.continue_watching,
|
||||
R.string.recently_released,
|
||||
R.string.recently_added,
|
||||
R.string.suggestions,
|
||||
R.string.top_unwatched,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -137,13 +214,16 @@ fun RecommendedMovie(
|
|||
onClickItem: (BaseItem) -> Unit,
|
||||
onFocusPosition: (RowColumn) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: RecommendedMovieViewModel = hiltViewModel(),
|
||||
viewModel: RecommendedMovieViewModel =
|
||||
hiltViewModel<RecommendedMovieViewModel, RecommendedMovieViewModel.Factory>(
|
||||
creationCallback = { it.create(parentId) },
|
||||
),
|
||||
) {
|
||||
OneTimeLaunchedEffect {
|
||||
viewModel.init(preferences, parentId)
|
||||
viewModel.init()
|
||||
}
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
val rows by viewModel.rows.observeAsState(listOf())
|
||||
val rows by viewModel.rows.collectAsState(listOf())
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -15,17 +17,24 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
|||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||
import com.github.damontecres.wholphin.ui.main.HomeRow
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetNextUpRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -36,41 +45,99 @@ import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
|||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@HiltViewModel(assistedFactory = RecommendedTvShowViewModel.Factory::class)
|
||||
class RecommendedTvShowViewModel
|
||||
@Inject
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
@Assisted val parentId: UUID,
|
||||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(parentId: UUID): RecommendedTvShowViewModel
|
||||
}
|
||||
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val rows = MutableLiveData<List<HomeRow>>()
|
||||
|
||||
fun init(
|
||||
preferences: UserPreferences,
|
||||
parentId: UUID,
|
||||
) {
|
||||
val rows =
|
||||
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
||||
rowTitles
|
||||
.map {
|
||||
HomeRowLoadingState.Pending(
|
||||
context.getString(it),
|
||||
)
|
||||
}.toMutableList(),
|
||||
)
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
enableUserData = true,
|
||||
)
|
||||
val resumeItems =
|
||||
ApiRequestPager(api, resumeItemsRequest, GetResumeItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
try {
|
||||
val resumeItemsRequest =
|
||||
GetResumeItemsRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||
enableUserData = true,
|
||||
)
|
||||
val resumeItems =
|
||||
ApiRequestPager(
|
||||
api,
|
||||
resumeItemsRequest,
|
||||
GetResumeItemsRequestHandler,
|
||||
viewModelScope,
|
||||
useSeriesForPrimary = true,
|
||||
).init()
|
||||
if (resumeItems.isNotEmpty()) {
|
||||
resumeItems.getBlocking(0)
|
||||
}
|
||||
|
||||
val nextUpRequest =
|
||||
GetNextUpRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
enableUserData = true,
|
||||
val nextUpRequest =
|
||||
GetNextUpRequest(
|
||||
parentId = parentId,
|
||||
fields = SlimItemFields,
|
||||
enableUserData = true,
|
||||
)
|
||||
val nextUpItems =
|
||||
ApiRequestPager(
|
||||
api,
|
||||
nextUpRequest,
|
||||
GetNextUpRequestHandler,
|
||||
viewModelScope,
|
||||
useSeriesForPrimary = true,
|
||||
).init()
|
||||
if (nextUpItems.isNotEmpty()) {
|
||||
nextUpItems.getBlocking(0)
|
||||
}
|
||||
|
||||
update(
|
||||
0,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.continue_watching),
|
||||
resumeItems,
|
||||
),
|
||||
)
|
||||
val nextUpItems = ApiRequestPager(api, nextUpRequest, GetNextUpRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
update(
|
||||
1,
|
||||
HomeRowLoadingState.Success(
|
||||
context.getString(R.string.next_up),
|
||||
nextUpItems,
|
||||
),
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
loading.value = LoadingState.Success
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception fetching tv recommendations")
|
||||
withContext(Dispatchers.Main) {
|
||||
loading.value = LoadingState.Error(ex)
|
||||
}
|
||||
}
|
||||
|
||||
val recentlyReleasedRequest =
|
||||
GetItemsRequest(
|
||||
|
|
@ -120,23 +187,50 @@ class RecommendedTvShowViewModel
|
|||
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||
|
||||
val rows =
|
||||
listOf(resumeItems, nextUpItems, recentlyReleasedItems, recentlyAddedItems, suggestedItems, unwatchedTopRatedItems)
|
||||
rows.forEach { it.init() }
|
||||
val homeRows =
|
||||
listOf(
|
||||
HomeRow(R.string.continue_watching, resumeItems),
|
||||
HomeRow(R.string.next_up, nextUpItems),
|
||||
HomeRow(R.string.recently_released, recentlyReleasedItems),
|
||||
HomeRow(R.string.recently_added, recentlyAddedItems),
|
||||
HomeRow(R.string.suggestions, suggestedItems),
|
||||
HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
|
||||
).filter { it.items.isNotEmpty() }
|
||||
withContext(Dispatchers.Main) {
|
||||
this@RecommendedTvShowViewModel.rows.value = homeRows
|
||||
loading.value = LoadingState.Success
|
||||
R.string.recently_released to recentlyReleasedItems,
|
||||
R.string.recently_added to recentlyAddedItems,
|
||||
R.string.suggestions to suggestedItems,
|
||||
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||
)
|
||||
|
||||
rows.forEachIndexed { index, (title, pager) ->
|
||||
viewModelScope.launchIO {
|
||||
val title = context.getString(title)
|
||||
val result =
|
||||
try {
|
||||
pager.init()
|
||||
HomeRowLoadingState.Success(title, pager)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching %s", title)
|
||||
HomeRowLoadingState.Error(title, null, ex)
|
||||
}
|
||||
update(index + 2, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(
|
||||
position: Int,
|
||||
row: HomeRowLoadingState,
|
||||
) {
|
||||
rows.update { current ->
|
||||
current.apply { set(position, row) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val rowTitles =
|
||||
listOf(
|
||||
R.string.continue_watching,
|
||||
R.string.next_up,
|
||||
R.string.recently_released,
|
||||
R.string.recently_added,
|
||||
R.string.suggestions,
|
||||
R.string.top_unwatched,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -149,13 +243,17 @@ fun RecommendedTvShow(
|
|||
onClickItem: (BaseItem) -> Unit,
|
||||
onFocusPosition: (RowColumn) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: RecommendedTvShowViewModel = hiltViewModel(),
|
||||
viewModel: RecommendedTvShowViewModel =
|
||||
hiltViewModel<RecommendedTvShowViewModel, RecommendedTvShowViewModel.Factory>(
|
||||
creationCallback = { it.create(parentId) },
|
||||
),
|
||||
) {
|
||||
OneTimeLaunchedEffect {
|
||||
viewModel.init(preferences, parentId)
|
||||
viewModel.init()
|
||||
}
|
||||
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
val rows by viewModel.rows.observeAsState(listOf())
|
||||
val rows by viewModel.rows.collectAsState(listOf())
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class MovieViewModel
|
|||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(itemId2: UUID): MovieViewModel
|
||||
fun create(itemId: UUID): MovieViewModel
|
||||
}
|
||||
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.github.damontecres.wholphin.ui.main
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -67,6 +66,7 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.formatDateTime
|
||||
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||
|
|
@ -77,22 +77,6 @@ import org.jellyfin.sdk.model.extensions.ticks
|
|||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class HomeRow(
|
||||
@param:StringRes val titleRes: Int?,
|
||||
val title: String?,
|
||||
val items: List<BaseItem?>,
|
||||
) {
|
||||
constructor(
|
||||
@StringRes titleRes: Int,
|
||||
items: List<BaseItem?>,
|
||||
) : this(titleRes, null, items)
|
||||
|
||||
constructor(
|
||||
title: String,
|
||||
items: List<BaseItem?>,
|
||||
) : this(null, title, items)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomePage(
|
||||
preferences: UserPreferences,
|
||||
|
|
@ -203,7 +187,7 @@ fun HomePage(
|
|||
|
||||
@Composable
|
||||
fun HomePageContent(
|
||||
homeRows: List<HomeRow>,
|
||||
homeRows: List<HomeRowLoadingState>,
|
||||
onClickItem: (BaseItem) -> Unit,
|
||||
onLongClickItem: (BaseItem) -> Unit,
|
||||
showClock: Boolean,
|
||||
|
|
@ -215,7 +199,10 @@ fun HomePageContent(
|
|||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
||||
mutableStateOf(RowColumn(0, 0))
|
||||
}
|
||||
var focusedItem = position.let { homeRows.getOrNull(it.row)?.items?.getOrNull(it.column) }
|
||||
var focusedItem =
|
||||
position.let {
|
||||
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
|
@ -284,55 +271,101 @@ fun HomePageContent(
|
|||
modifier = Modifier,
|
||||
) {
|
||||
itemsIndexed(homeRows) { rowIndex, row ->
|
||||
if (row.items.isNotEmpty()) {
|
||||
ItemRow(
|
||||
title = row.title ?: row.titleRes?.let { stringResource(it) } ?: "",
|
||||
items = row.items,
|
||||
onClickItem = onClickItem,
|
||||
cardOnFocus = { isFocused, index ->
|
||||
if (isFocused) {
|
||||
focusedItem = row.items.getOrNull(index)
|
||||
position = RowColumn(rowIndex, index)
|
||||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
// TODO better aspect ration handling?
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
imageUrl = item?.imageUrl,
|
||||
aspectRatio = (2f / 3f),
|
||||
cornerText =
|
||||
item?.data?.indexNumber?.let { "E$it" }
|
||||
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
playPercent = item?.data?.userData?.playedPercentage ?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier =
|
||||
cardModifier
|
||||
.ifElse(
|
||||
focusedItem == item,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).ifElse(
|
||||
RowColumn(rowIndex, index) == position,
|
||||
Modifier.focusRequester(positionFocusRequester),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
onFocusPosition?.invoke(
|
||||
RowColumn(
|
||||
rowIndex,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
interactionSource = null,
|
||||
cardHeight = Cards.height2x3,
|
||||
when (val r = row) {
|
||||
is HomeRowLoadingState.Loading,
|
||||
is HomeRowLoadingState.Pending,
|
||||
-> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.loading),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowLoadingState.Error -> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = r.localizedMessage,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowLoadingState.Success -> {
|
||||
if (row.items.isNotEmpty()) {
|
||||
ItemRow(
|
||||
title = row.title,
|
||||
items = row.items,
|
||||
onClickItem = onClickItem,
|
||||
cardOnFocus = { isFocused, index ->
|
||||
if (isFocused) {
|
||||
focusedItem = row.items.getOrNull(index)
|
||||
position = RowColumn(rowIndex, index)
|
||||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
// TODO better aspect ration handling?
|
||||
BannerCard(
|
||||
name = item?.data?.seriesName ?: item?.name,
|
||||
imageUrl = item?.imageUrl,
|
||||
aspectRatio = (2f / 3f),
|
||||
cornerText =
|
||||
item?.data?.indexNumber?.let { "E$it" }
|
||||
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
|
||||
played = item?.data?.userData?.played ?: false,
|
||||
playPercent =
|
||||
item?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier =
|
||||
cardModifier
|
||||
.ifElse(
|
||||
focusedItem == item,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).ifElse(
|
||||
RowColumn(rowIndex, index) == position,
|
||||
Modifier.focusRequester(
|
||||
positionFocusRequester,
|
||||
),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
onFocusPosition?.invoke(
|
||||
RowColumn(
|
||||
rowIndex,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
interactionSource = null,
|
||||
cardHeight = Cards.height2x3,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.ui.SlimItemFields
|
|||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||
|
|
@ -50,7 +51,7 @@ class HomeViewModel
|
|||
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
) : ViewModel() {
|
||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val homeRows = MutableLiveData<List<HomeRow>>()
|
||||
val homeRows = MutableLiveData<List<HomeRowLoadingState>>()
|
||||
|
||||
private lateinit var preferences: UserPreferences
|
||||
|
||||
|
|
@ -89,25 +90,21 @@ class HomeViewModel
|
|||
buildList {
|
||||
if (resume.isNotEmpty()) {
|
||||
add(
|
||||
HomeRow(
|
||||
titleRes = R.string.continue_watching,
|
||||
HomeRowLoadingState.Success(
|
||||
title = context.getString(R.string.continue_watching),
|
||||
items = resume,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (nextUp.isNotEmpty()) {
|
||||
add(
|
||||
HomeRow(
|
||||
titleRes = R.string.next_up,
|
||||
HomeRowLoadingState.Success(
|
||||
title = context.getString(R.string.next_up),
|
||||
items = nextUp,
|
||||
),
|
||||
)
|
||||
}
|
||||
latest.forEach {
|
||||
if (it.items.isNotEmpty()) {
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
addAll(latest)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
this@HomeViewModel.homeRows.value = homeRows
|
||||
|
|
@ -177,7 +174,7 @@ class HomeViewModel
|
|||
user: UserDto,
|
||||
limit: Int,
|
||||
includedIds: List<UUID>,
|
||||
): List<HomeRow> {
|
||||
): List<HomeRowLoadingState> {
|
||||
val latestMediaIncludes =
|
||||
user.configuration
|
||||
?.orderedViews
|
||||
|
|
@ -199,38 +196,47 @@ class HomeViewModel
|
|||
} else {
|
||||
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
||||
} ?: context.getString(R.string.recently_added)
|
||||
val viewId =
|
||||
if (view.collectionType == CollectionType.LIVETV) {
|
||||
api.liveTvApi
|
||||
.getRecordingFolders(
|
||||
userId = user.id,
|
||||
).content.items
|
||||
.firstOrNull()
|
||||
?.id
|
||||
} else {
|
||||
view.id
|
||||
}
|
||||
viewId?.let {
|
||||
val request =
|
||||
GetLatestMediaRequest(
|
||||
fields = SlimItemFields,
|
||||
imageTypeLimit = 1,
|
||||
parentId = viewId,
|
||||
groupItems = true,
|
||||
limit = limit,
|
||||
isPlayed = null, // Server will handle user's preference
|
||||
try {
|
||||
val viewId =
|
||||
if (view.collectionType == CollectionType.LIVETV) {
|
||||
api.liveTvApi
|
||||
.getRecordingFolders(
|
||||
userId = user.id,
|
||||
).content.items
|
||||
.firstOrNull()
|
||||
?.id
|
||||
} else {
|
||||
view.id
|
||||
}
|
||||
viewId?.let {
|
||||
val request =
|
||||
GetLatestMediaRequest(
|
||||
fields = SlimItemFields,
|
||||
imageTypeLimit = 1,
|
||||
parentId = viewId,
|
||||
groupItems = true,
|
||||
limit = limit,
|
||||
isPlayed = null, // Server will handle user's preference
|
||||
)
|
||||
val latest =
|
||||
api.userLibraryApi
|
||||
.getLatestMedia(request)
|
||||
.content
|
||||
.map { BaseItem.from(it, api, true) }
|
||||
HomeRowLoadingState.Success(
|
||||
title = title,
|
||||
items = latest,
|
||||
)
|
||||
val latest =
|
||||
api.userLibraryApi
|
||||
.getLatestMedia(request)
|
||||
.content
|
||||
.map { BaseItem.from(it, api, true) }
|
||||
HomeRow(
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception fetching %s", title)
|
||||
HomeRowLoadingState.Error(
|
||||
title = title,
|
||||
items = latest,
|
||||
exception = ex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,3 +42,29 @@ sealed interface RowLoadingState {
|
|||
listOfNotNull(message, exception?.localizedMessage).joinToString(" - ")
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface HomeRowLoadingState {
|
||||
val title: String
|
||||
|
||||
data class Pending(
|
||||
override val title: String,
|
||||
) : HomeRowLoadingState
|
||||
|
||||
data class Loading(
|
||||
override val title: String,
|
||||
) : HomeRowLoadingState
|
||||
|
||||
data class Success(
|
||||
override val title: String,
|
||||
val items: List<BaseItem?>,
|
||||
) : HomeRowLoadingState
|
||||
|
||||
data class Error(
|
||||
override val title: String,
|
||||
val message: String? = null,
|
||||
val exception: Throwable? = null,
|
||||
) : HomeRowLoadingState {
|
||||
val localizedMessage: String =
|
||||
listOfNotNull(message, exception?.localizedMessage).joinToString(" - ")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue