Merge branch 'main' into develop/mpv

This commit is contained in:
Damontecres 2025-11-10 10:16:14 -05:00
commit ad80f5bbe4
No known key found for this signature in database
10 changed files with 469 additions and 207 deletions

View file

@ -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,24 +43,38 @@ 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()) {
try {
val resumeItemsRequest =
GetResumeItemsRequest(
parentId = parentId,
@ -60,7 +83,33 @@ class RecommendedMovieViewModel
enableUserData = true,
)
val resumeItems =
ApiRequestPager(api, resumeItemsRequest, GetResumeItemsRequestHandler, viewModelScope)
ApiRequestPager(
api,
resumeItemsRequest,
GetResumeItemsRequestHandler,
viewModelScope,
).init()
if (resumeItems.isNotEmpty()) {
resumeItems.getBlocking(0)
}
update(
0,
HomeRowLoadingState.Success(
context.getString(R.string.continue_watching),
resumeItems,
),
)
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,24 +158,52 @@ 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,
)
}
}
/**
* The "recommended" tab of a movie library
*/
@ -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)

View file

@ -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,24 +45,38 @@ 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() {
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
val rows = MutableLiveData<List<HomeRow>>()
@AssistedFactory
interface Factory {
fun create(parentId: UUID): RecommendedTvShowViewModel
}
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()) {
try {
val resumeItemsRequest =
GetResumeItemsRequest(
parentId = parentId,
@ -62,7 +85,16 @@ class RecommendedTvShowViewModel
enableUserData = true,
)
val resumeItems =
ApiRequestPager(api, resumeItemsRequest, GetResumeItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
ApiRequestPager(
api,
resumeItemsRequest,
GetResumeItemsRequestHandler,
viewModelScope,
useSeriesForPrimary = true,
).init()
if (resumeItems.isNotEmpty()) {
resumeItems.getBlocking(0)
}
val nextUpRequest =
GetNextUpRequest(
@ -70,7 +102,42 @@ class RecommendedTvShowViewModel
fields = SlimItemFields,
enableUserData = true,
)
val nextUpItems = ApiRequestPager(api, nextUpRequest, GetNextUpRequestHandler, viewModelScope, useSeriesForPrimary = 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,
),
)
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,25 +187,52 @@ 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,
)
}
}
/**
* The "recommended" tab of a TV show library
*/
@ -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)

View file

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

View file

@ -24,18 +24,18 @@ import org.jellyfin.sdk.model.extensions.ticks
@Composable
fun FocusedEpisodeHeader(
ep: BaseItem,
ep: BaseItem?,
overviewOnClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val dto = ep.data
val dto = ep?.data
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier,
) {
Text(
text = dto.episodeTitle ?: dto.name ?: "",
text = dto?.episodeTitle ?: dto?.name ?: "",
style = MaterialTheme.typography.headlineSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -46,17 +46,17 @@ fun FocusedEpisodeHeader(
) {
val details =
buildList {
dto.seasonEpisode?.let(::add)
dto.premiereDate?.let { add(formatDateTime(it)) }
val duration = dto.runTimeTicks?.ticks
dto?.seasonEpisode?.let(::add)
dto?.premiereDate?.let { add(formatDateTime(it)) }
val duration = dto?.runTimeTicks?.ticks
duration
?.roundMinutes
?.toString()
?.let {
add(it)
}
dto.officialRating?.let(::add)
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
dto?.officialRating?.let(::add)
dto?.timeRemaining?.roundMinutes?.let { add("$it left") }
}
DotSeparatedRow(
texts = details,
@ -68,12 +68,12 @@ fun FocusedEpisodeHeader(
verticalAlignment = Alignment.CenterVertically,
) {
SimpleStarRating(
dto.communityRating,
dto?.communityRating,
Modifier.height(20.dp),
)
}
OverviewText(
overview = dto.overview ?: "",
overview = dto?.overview ?: "",
maxLines = 3,
onClick = overviewOnClick,
)

View file

@ -162,14 +162,12 @@ fun SeriesOverviewContent(
}
item {
// Episode header
focusedEpisode?.let { ep ->
FocusedEpisodeHeader(
ep = ep,
ep = focusedEpisode,
overviewOnClick = overviewOnClick,
modifier = Modifier.fillMaxWidth(.66f),
)
}
}
item {
key(position.seasonTabIndex) {
when (val eps = episodes) {

View file

@ -293,17 +293,11 @@ class SeriesViewModel
itemId: UUID,
listIndex: Int,
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
val base = api.userLibraryApi.getItem(itemId).content
val item = BaseItem.Companion.from(base, api)
val eps = episodes.value
if (eps is EpisodeList.Success) {
val newItems =
eps.episodes.toMutableList().apply {
this[listIndex] = item
}
val newValue = eps.copy(episodes = newItems)
eps.episodes.refreshItem(listIndex, itemId)
withContext(Dispatchers.Main) {
episodes.value = newValue
episodes.value = eps
}
}
}
@ -409,7 +403,7 @@ sealed interface EpisodeList {
}
data class Success(
val episodes: List<BaseItem?>,
val episodes: ApiRequestPager<GetEpisodesRequest>,
val initialIndex: Int,
) : EpisodeList
}

View file

@ -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,9 +271,49 @@ fun HomePageContent(
modifier = Modifier,
) {
itemsIndexed(homeRows) { rowIndex, row ->
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 ?: row.titleRes?.let { stringResource(it) } ?: "",
title = row.title,
items = row.items,
onClickItem = onClickItem,
cardOnFocus = { isFocused, index ->
@ -307,7 +334,9 @@ fun HomePageContent(
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,
playPercent =
item?.data?.userData?.playedPercentage
?: 0.0,
onClick = onClick,
onLongClick = onLongClick,
modifier =
@ -317,7 +346,9 @@ fun HomePageContent(
Modifier.focusRequester(focusRequester),
).ifElse(
RowColumn(rowIndex, index) == position,
Modifier.focusRequester(positionFocusRequester),
Modifier.focusRequester(
positionFocusRequester,
),
).onFocusChanged {
if (it.isFocused) {
onFocusPosition?.invoke(
@ -337,6 +368,8 @@ fun HomePageContent(
}
}
}
}
}
when (loadingState) {
LoadingState.Pending,
LoadingState.Loading,

View file

@ -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,6 +196,7 @@ class HomeViewModel
} else {
view.name?.let { context.getString(R.string.recently_added_in, it) }
} ?: context.getString(R.string.recently_added)
try {
val viewId =
if (view.collectionType == CollectionType.LIVETV) {
api.liveTvApi
@ -225,12 +223,20 @@ class HomeViewModel
.getLatestMedia(request)
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
HomeRowLoadingState.Success(
title = title,
items = latest,
)
}
} catch (ex: Exception) {
Timber.e(ex, "Exception fetching %s", title)
HomeRowLoadingState.Error(
title = title,
exception = ex,
)
}
}
return rows
}

View file

@ -21,6 +21,7 @@ import org.jellyfin.sdk.api.client.extensions.liveTvApi
import org.jellyfin.sdk.api.client.extensions.playlistsApi
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
import org.jellyfin.sdk.model.api.GetProgramsDto
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
@ -31,6 +32,7 @@ import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest
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 java.util.function.Predicate
/**
@ -58,7 +60,7 @@ class ApiRequestPager<T>(
CacheBuilder
.newBuilder()
.maximumSize(cacheSize)
.build<Int, List<BaseItem>>()
.build<Int, MutableList<BaseItem>>()
suspend fun init(): ApiRequestPager<T> {
if (totalCount < 0) {
@ -122,13 +124,38 @@ class ApiRequestPager<T>(
false,
)
val result = requestHandler.execute(api, newRequest).content
val data = result.items.map { BaseItem.from(it, api, useSeriesForPrimary) }
val data = mutableListOf<BaseItem>()
result.items.forEach { data.add(BaseItem.from(it, api, useSeriesForPrimary)) }
cachedPages.put(pageNumber, data)
items = ItemList(totalCount, pageSize, cachedPages.asMap())
}
}
}
suspend fun refreshItem(
position: Int,
itemId: UUID,
) {
val item =
api.userLibraryApi.getItem(itemId).content.let {
BaseItem.from(
it,
api,
useSeriesForPrimary,
)
}
val pageNumber = position / pageSize
val index = position - pageNumber * pageSize
mutex.withLock {
val page = cachedPages.getIfPresent(pageNumber)
if (page != null && index in page.indices) {
page[index] = item
cachedPages.put(pageNumber, page)
items = ItemList(totalCount, pageSize, cachedPages.asMap())
}
}
}
companion object {
private const val DEBUG = false
}

View file

@ -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(" - ")
}
}