mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Merge branch 'main' into develop/mpv
This commit is contained in:
commit
ad80f5bbe4
10 changed files with 469 additions and 207 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,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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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