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
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
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.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
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.HomePageContent
|
||||||
import com.github.damontecres.wholphin.ui.main.HomeRow
|
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
||||||
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
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.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
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.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel(assistedFactory = RecommendedMovieViewModel.Factory::class)
|
||||||
class RecommendedMovieViewModel
|
class RecommendedMovieViewModel
|
||||||
@Inject
|
@AssistedInject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
|
@Assisted val parentId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
@AssistedFactory
|
||||||
val rows = MutableLiveData<List<HomeRow>>()
|
interface Factory {
|
||||||
|
fun create(parentId: UUID): RecommendedMovieViewModel
|
||||||
|
}
|
||||||
|
|
||||||
fun init(
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
preferences: UserPreferences,
|
|
||||||
parentId: UUID,
|
val rows =
|
||||||
) {
|
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
||||||
|
rowTitles
|
||||||
|
.map {
|
||||||
|
HomeRowLoadingState.Pending(
|
||||||
|
context.getString(it),
|
||||||
|
)
|
||||||
|
}.toMutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun init() {
|
||||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
|
try {
|
||||||
val resumeItemsRequest =
|
val resumeItemsRequest =
|
||||||
GetResumeItemsRequest(
|
GetResumeItemsRequest(
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
|
|
@ -60,7 +83,33 @@ class RecommendedMovieViewModel
|
||||||
enableUserData = true,
|
enableUserData = true,
|
||||||
)
|
)
|
||||||
val resumeItems =
|
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 =
|
val recentlyReleasedRequest =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
|
|
@ -109,24 +158,52 @@ class RecommendedMovieViewModel
|
||||||
val unwatchedTopRatedItems =
|
val unwatchedTopRatedItems =
|
||||||
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||||
|
|
||||||
val rows = listOf(resumeItems, recentlyReleasedItems, recentlyAddedItems, suggestedItems, unwatchedTopRatedItems)
|
val rows =
|
||||||
rows.forEach { it.init() }
|
|
||||||
val homeRows =
|
|
||||||
listOf(
|
listOf(
|
||||||
HomeRow(R.string.continue_watching, resumeItems),
|
R.string.recently_released to recentlyReleasedItems,
|
||||||
HomeRow(R.string.recently_released, recentlyReleasedItems),
|
R.string.recently_added to recentlyAddedItems,
|
||||||
HomeRow(R.string.recently_added, recentlyAddedItems),
|
R.string.suggestions to suggestedItems,
|
||||||
HomeRow(R.string.suggestions, suggestedItems),
|
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||||
HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
|
)
|
||||||
).filter { it.items.isNotEmpty() }
|
|
||||||
withContext(Dispatchers.Main) {
|
rows.forEachIndexed { index, (title, pager) ->
|
||||||
this@RecommendedMovieViewModel.rows.value = homeRows
|
viewModelScope.launchIO {
|
||||||
loading.value = LoadingState.Success
|
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
|
* The "recommended" tab of a movie library
|
||||||
*/
|
*/
|
||||||
|
|
@ -137,13 +214,16 @@ fun RecommendedMovie(
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
onFocusPosition: (RowColumn) -> Unit,
|
onFocusPosition: (RowColumn) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: RecommendedMovieViewModel = hiltViewModel(),
|
viewModel: RecommendedMovieViewModel =
|
||||||
|
hiltViewModel<RecommendedMovieViewModel, RecommendedMovieViewModel.Factory>(
|
||||||
|
creationCallback = { it.create(parentId) },
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
viewModel.init(preferences, parentId)
|
viewModel.init()
|
||||||
}
|
}
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
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) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
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.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
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.HomePageContent
|
||||||
import com.github.damontecres.wholphin.ui.main.HomeRow
|
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetNextUpRequestHandler
|
import com.github.damontecres.wholphin.util.GetNextUpRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler
|
||||||
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
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.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
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.GetNextUpRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel(assistedFactory = RecommendedTvShowViewModel.Factory::class)
|
||||||
class RecommendedTvShowViewModel
|
class RecommendedTvShowViewModel
|
||||||
@Inject
|
@AssistedInject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
|
@Assisted val parentId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
@AssistedFactory
|
||||||
val rows = MutableLiveData<List<HomeRow>>()
|
interface Factory {
|
||||||
|
fun create(parentId: UUID): RecommendedTvShowViewModel
|
||||||
|
}
|
||||||
|
|
||||||
fun init(
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
preferences: UserPreferences,
|
|
||||||
parentId: UUID,
|
val rows =
|
||||||
) {
|
MutableStateFlow<MutableList<HomeRowLoadingState>>(
|
||||||
|
rowTitles
|
||||||
|
.map {
|
||||||
|
HomeRowLoadingState.Pending(
|
||||||
|
context.getString(it),
|
||||||
|
)
|
||||||
|
}.toMutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun init() {
|
||||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
|
try {
|
||||||
val resumeItemsRequest =
|
val resumeItemsRequest =
|
||||||
GetResumeItemsRequest(
|
GetResumeItemsRequest(
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
|
|
@ -62,7 +85,16 @@ class RecommendedTvShowViewModel
|
||||||
enableUserData = true,
|
enableUserData = true,
|
||||||
)
|
)
|
||||||
val resumeItems =
|
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 =
|
val nextUpRequest =
|
||||||
GetNextUpRequest(
|
GetNextUpRequest(
|
||||||
|
|
@ -70,7 +102,42 @@ class RecommendedTvShowViewModel
|
||||||
fields = SlimItemFields,
|
fields = SlimItemFields,
|
||||||
enableUserData = true,
|
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 =
|
val recentlyReleasedRequest =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
|
|
@ -120,25 +187,52 @@ class RecommendedTvShowViewModel
|
||||||
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
ApiRequestPager(api, unwatchedTopRatedRequest, GetItemsRequestHandler, viewModelScope, useSeriesForPrimary = true)
|
||||||
|
|
||||||
val rows =
|
val rows =
|
||||||
listOf(resumeItems, nextUpItems, recentlyReleasedItems, recentlyAddedItems, suggestedItems, unwatchedTopRatedItems)
|
|
||||||
rows.forEach { it.init() }
|
|
||||||
val homeRows =
|
|
||||||
listOf(
|
listOf(
|
||||||
HomeRow(R.string.continue_watching, resumeItems),
|
R.string.recently_released to recentlyReleasedItems,
|
||||||
HomeRow(R.string.next_up, nextUpItems),
|
R.string.recently_added to recentlyAddedItems,
|
||||||
HomeRow(R.string.recently_released, recentlyReleasedItems),
|
R.string.suggestions to suggestedItems,
|
||||||
HomeRow(R.string.recently_added, recentlyAddedItems),
|
R.string.top_unwatched to unwatchedTopRatedItems,
|
||||||
HomeRow(R.string.suggestions, suggestedItems),
|
)
|
||||||
HomeRow(R.string.top_unwatched, unwatchedTopRatedItems),
|
|
||||||
).filter { it.items.isNotEmpty() }
|
rows.forEachIndexed { index, (title, pager) ->
|
||||||
withContext(Dispatchers.Main) {
|
viewModelScope.launchIO {
|
||||||
this@RecommendedTvShowViewModel.rows.value = homeRows
|
val title = context.getString(title)
|
||||||
loading.value = LoadingState.Success
|
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
|
* The "recommended" tab of a TV show library
|
||||||
*/
|
*/
|
||||||
|
|
@ -149,13 +243,17 @@ fun RecommendedTvShow(
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
onFocusPosition: (RowColumn) -> Unit,
|
onFocusPosition: (RowColumn) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: RecommendedTvShowViewModel = hiltViewModel(),
|
viewModel: RecommendedTvShowViewModel =
|
||||||
|
hiltViewModel<RecommendedTvShowViewModel, RecommendedTvShowViewModel.Factory>(
|
||||||
|
creationCallback = { it.create(parentId) },
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
viewModel.init(preferences, parentId)
|
viewModel.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
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) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state)
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class MovieViewModel
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(itemId2: UUID): MovieViewModel
|
fun create(itemId: UUID): MovieViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,18 @@ import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FocusedEpisodeHeader(
|
fun FocusedEpisodeHeader(
|
||||||
ep: BaseItem,
|
ep: BaseItem?,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val dto = ep.data
|
val dto = ep?.data
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = dto.episodeTitle ?: dto.name ?: "",
|
text = dto?.episodeTitle ?: dto?.name ?: "",
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|
@ -46,17 +46,17 @@ fun FocusedEpisodeHeader(
|
||||||
) {
|
) {
|
||||||
val details =
|
val details =
|
||||||
buildList {
|
buildList {
|
||||||
dto.seasonEpisode?.let(::add)
|
dto?.seasonEpisode?.let(::add)
|
||||||
dto.premiereDate?.let { add(formatDateTime(it)) }
|
dto?.premiereDate?.let { add(formatDateTime(it)) }
|
||||||
val duration = dto.runTimeTicks?.ticks
|
val duration = dto?.runTimeTicks?.ticks
|
||||||
duration
|
duration
|
||||||
?.roundMinutes
|
?.roundMinutes
|
||||||
?.toString()
|
?.toString()
|
||||||
?.let {
|
?.let {
|
||||||
add(it)
|
add(it)
|
||||||
}
|
}
|
||||||
dto.officialRating?.let(::add)
|
dto?.officialRating?.let(::add)
|
||||||
dto.timeRemaining?.roundMinutes?.let { add("$it left") }
|
dto?.timeRemaining?.roundMinutes?.let { add("$it left") }
|
||||||
}
|
}
|
||||||
DotSeparatedRow(
|
DotSeparatedRow(
|
||||||
texts = details,
|
texts = details,
|
||||||
|
|
@ -68,12 +68,12 @@ fun FocusedEpisodeHeader(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
SimpleStarRating(
|
SimpleStarRating(
|
||||||
dto.communityRating,
|
dto?.communityRating,
|
||||||
Modifier.height(20.dp),
|
Modifier.height(20.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OverviewText(
|
OverviewText(
|
||||||
overview = dto.overview ?: "",
|
overview = dto?.overview ?: "",
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
onClick = overviewOnClick,
|
onClick = overviewOnClick,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -162,14 +162,12 @@ fun SeriesOverviewContent(
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
// Episode header
|
// Episode header
|
||||||
focusedEpisode?.let { ep ->
|
|
||||||
FocusedEpisodeHeader(
|
FocusedEpisodeHeader(
|
||||||
ep = ep,
|
ep = focusedEpisode,
|
||||||
overviewOnClick = overviewOnClick,
|
overviewOnClick = overviewOnClick,
|
||||||
modifier = Modifier.fillMaxWidth(.66f),
|
modifier = Modifier.fillMaxWidth(.66f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
key(position.seasonTabIndex) {
|
key(position.seasonTabIndex) {
|
||||||
when (val eps = episodes) {
|
when (val eps = episodes) {
|
||||||
|
|
|
||||||
|
|
@ -293,17 +293,11 @@ class SeriesViewModel
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
listIndex: Int,
|
listIndex: Int,
|
||||||
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
val base = api.userLibraryApi.getItem(itemId).content
|
|
||||||
val item = BaseItem.Companion.from(base, api)
|
|
||||||
val eps = episodes.value
|
val eps = episodes.value
|
||||||
if (eps is EpisodeList.Success) {
|
if (eps is EpisodeList.Success) {
|
||||||
val newItems =
|
eps.episodes.refreshItem(listIndex, itemId)
|
||||||
eps.episodes.toMutableList().apply {
|
|
||||||
this[listIndex] = item
|
|
||||||
}
|
|
||||||
val newValue = eps.copy(episodes = newItems)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
episodes.value = newValue
|
episodes.value = eps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -409,7 +403,7 @@ sealed interface EpisodeList {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val episodes: List<BaseItem?>,
|
val episodes: ApiRequestPager<GetEpisodesRequest>,
|
||||||
val initialIndex: Int,
|
val initialIndex: Int,
|
||||||
) : EpisodeList
|
) : EpisodeList
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.github.damontecres.wholphin.ui.main
|
package com.github.damontecres.wholphin.ui.main
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.roundMinutes
|
||||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
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.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.formatDateTime
|
import com.github.damontecres.wholphin.util.formatDateTime
|
||||||
import com.github.damontecres.wholphin.util.seasonEpisode
|
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||||
|
|
@ -77,22 +77,6 @@ import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
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
|
@Composable
|
||||||
fun HomePage(
|
fun HomePage(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
|
|
@ -203,7 +187,7 @@ fun HomePage(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePageContent(
|
fun HomePageContent(
|
||||||
homeRows: List<HomeRow>,
|
homeRows: List<HomeRowLoadingState>,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (BaseItem) -> Unit,
|
||||||
showClock: Boolean,
|
showClock: Boolean,
|
||||||
|
|
@ -215,7 +199,10 @@ fun HomePageContent(
|
||||||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
||||||
mutableStateOf(RowColumn(0, 0))
|
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 listState = rememberLazyListState()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
@ -284,9 +271,49 @@ fun HomePageContent(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
itemsIndexed(homeRows) { rowIndex, row ->
|
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()) {
|
if (row.items.isNotEmpty()) {
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = row.title ?: row.titleRes?.let { stringResource(it) } ?: "",
|
title = row.title,
|
||||||
items = row.items,
|
items = row.items,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
cardOnFocus = { isFocused, index ->
|
cardOnFocus = { isFocused, index ->
|
||||||
|
|
@ -307,7 +334,9 @@ fun HomePageContent(
|
||||||
item?.data?.indexNumber?.let { "E$it" }
|
item?.data?.indexNumber?.let { "E$it" }
|
||||||
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
|
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },
|
||||||
played = item?.data?.userData?.played ?: false,
|
played = item?.data?.userData?.played ?: false,
|
||||||
playPercent = item?.data?.userData?.playedPercentage ?: 0.0,
|
playPercent =
|
||||||
|
item?.data?.userData?.playedPercentage
|
||||||
|
?: 0.0,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -317,7 +346,9 @@ fun HomePageContent(
|
||||||
Modifier.focusRequester(focusRequester),
|
Modifier.focusRequester(focusRequester),
|
||||||
).ifElse(
|
).ifElse(
|
||||||
RowColumn(rowIndex, index) == position,
|
RowColumn(rowIndex, index) == position,
|
||||||
Modifier.focusRequester(positionFocusRequester),
|
Modifier.focusRequester(
|
||||||
|
positionFocusRequester,
|
||||||
|
),
|
||||||
).onFocusChanged {
|
).onFocusChanged {
|
||||||
if (it.isFocused) {
|
if (it.isFocused) {
|
||||||
onFocusPosition?.invoke(
|
onFocusPosition?.invoke(
|
||||||
|
|
@ -337,6 +368,8 @@ fun HomePageContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
when (loadingState) {
|
when (loadingState) {
|
||||||
LoadingState.Pending,
|
LoadingState.Pending,
|
||||||
LoadingState.Loading,
|
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.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
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.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||||
|
|
@ -50,7 +51,7 @@ class HomeViewModel
|
||||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val homeRows = MutableLiveData<List<HomeRow>>()
|
val homeRows = MutableLiveData<List<HomeRowLoadingState>>()
|
||||||
|
|
||||||
private lateinit var preferences: UserPreferences
|
private lateinit var preferences: UserPreferences
|
||||||
|
|
||||||
|
|
@ -89,25 +90,21 @@ class HomeViewModel
|
||||||
buildList {
|
buildList {
|
||||||
if (resume.isNotEmpty()) {
|
if (resume.isNotEmpty()) {
|
||||||
add(
|
add(
|
||||||
HomeRow(
|
HomeRowLoadingState.Success(
|
||||||
titleRes = R.string.continue_watching,
|
title = context.getString(R.string.continue_watching),
|
||||||
items = resume,
|
items = resume,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (nextUp.isNotEmpty()) {
|
if (nextUp.isNotEmpty()) {
|
||||||
add(
|
add(
|
||||||
HomeRow(
|
HomeRowLoadingState.Success(
|
||||||
titleRes = R.string.next_up,
|
title = context.getString(R.string.next_up),
|
||||||
items = nextUp,
|
items = nextUp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
latest.forEach {
|
addAll(latest)
|
||||||
if (it.items.isNotEmpty()) {
|
|
||||||
add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@HomeViewModel.homeRows.value = homeRows
|
this@HomeViewModel.homeRows.value = homeRows
|
||||||
|
|
@ -177,7 +174,7 @@ class HomeViewModel
|
||||||
user: UserDto,
|
user: UserDto,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
includedIds: List<UUID>,
|
includedIds: List<UUID>,
|
||||||
): List<HomeRow> {
|
): List<HomeRowLoadingState> {
|
||||||
val latestMediaIncludes =
|
val latestMediaIncludes =
|
||||||
user.configuration
|
user.configuration
|
||||||
?.orderedViews
|
?.orderedViews
|
||||||
|
|
@ -199,6 +196,7 @@ class HomeViewModel
|
||||||
} else {
|
} else {
|
||||||
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
||||||
} ?: context.getString(R.string.recently_added)
|
} ?: context.getString(R.string.recently_added)
|
||||||
|
try {
|
||||||
val viewId =
|
val viewId =
|
||||||
if (view.collectionType == CollectionType.LIVETV) {
|
if (view.collectionType == CollectionType.LIVETV) {
|
||||||
api.liveTvApi
|
api.liveTvApi
|
||||||
|
|
@ -225,12 +223,20 @@ class HomeViewModel
|
||||||
.getLatestMedia(request)
|
.getLatestMedia(request)
|
||||||
.content
|
.content
|
||||||
.map { BaseItem.from(it, api, true) }
|
.map { BaseItem.from(it, api, true) }
|
||||||
HomeRow(
|
HomeRowLoadingState.Success(
|
||||||
title = title,
|
title = title,
|
||||||
items = latest,
|
items = latest,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Exception fetching %s", title)
|
||||||
|
HomeRowLoadingState.Error(
|
||||||
|
title = title,
|
||||||
|
exception = ex,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rows
|
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.playlistsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
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.BaseItemDtoQueryResult
|
||||||
import org.jellyfin.sdk.model.api.GetProgramsDto
|
import org.jellyfin.sdk.model.api.GetProgramsDto
|
||||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
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.GetResumeItemsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,7 +60,7 @@ class ApiRequestPager<T>(
|
||||||
CacheBuilder
|
CacheBuilder
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.maximumSize(cacheSize)
|
.maximumSize(cacheSize)
|
||||||
.build<Int, List<BaseItem>>()
|
.build<Int, MutableList<BaseItem>>()
|
||||||
|
|
||||||
suspend fun init(): ApiRequestPager<T> {
|
suspend fun init(): ApiRequestPager<T> {
|
||||||
if (totalCount < 0) {
|
if (totalCount < 0) {
|
||||||
|
|
@ -122,13 +124,38 @@ class ApiRequestPager<T>(
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
val result = requestHandler.execute(api, newRequest).content
|
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)
|
cachedPages.put(pageNumber, data)
|
||||||
items = ItemList(totalCount, pageSize, cachedPages.asMap())
|
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 {
|
companion object {
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,3 +42,29 @@ sealed interface RowLoadingState {
|
||||||
listOfNotNull(message, exception?.localizedMessage).joinToString(" - ")
|
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