Add paging support to LibraryFragment (#124)
* Add paging support to the LibraryFragment * Fix error handling
This commit is contained in:
parent
82b235d3ae
commit
16c2cd634d
7 changed files with 204 additions and 9 deletions
|
@ -124,4 +124,7 @@ dependencies {
|
|||
val aboutLibrariesVersion = "10.3.0"
|
||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibrariesVersion")
|
||||
implementation("com.mikepenz:aboutlibraries:$aboutLibrariesVersion")
|
||||
|
||||
val pagingVersion = "3.1.1"
|
||||
implementation("androidx.paging:paging-runtime-ktx:$pagingVersion")
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package dev.jdtech.jellyfin.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.databinding.BaseItemBinding
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
||||
class ViewItemPagingAdapter(
|
||||
private val onClickListener: OnClickListener,
|
||||
private val fixedWidth: Boolean = false,
|
||||
) : PagingDataAdapter<BaseItemDto, ViewItemPagingAdapter.ItemViewHolder>(DiffCallback) {
|
||||
|
||||
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: BaseItemDto, fixedWidth: Boolean) {
|
||||
binding.item = item
|
||||
binding.itemName.text =
|
||||
if (item.type == BaseItemKind.EPISODE) item.seriesName else item.name
|
||||
binding.itemCount.visibility =
|
||||
if (item.userData?.unplayedItemCount != null && item.userData?.unplayedItemCount!! > 0) View.VISIBLE else View.GONE
|
||||
if (fixedWidth) {
|
||||
binding.itemLayout.layoutParams.width =
|
||||
parent.resources.getDimension(R.dimen.overview_media_width).toInt()
|
||||
(binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
|
||||
}
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<BaseItemDto>() {
|
||||
override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
|
||||
return ItemViewHolder(
|
||||
BaseItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
), parent
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
if (item != null) {
|
||||
holder.itemView.setOnClickListener {
|
||||
onClickListener.onClick(item)
|
||||
}
|
||||
holder.bind(item, fixedWidth)
|
||||
}
|
||||
}
|
||||
|
||||
class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) {
|
||||
fun onClick(item: BaseItemDto) = clickListener(item)
|
||||
}
|
||||
}
|
|
@ -11,10 +11,11 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.paging.LoadState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
|
||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.ViewItemPagingAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
||||
|
@ -91,10 +92,25 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding.itemsRecyclerView.adapter =
|
||||
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
|
||||
ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item ->
|
||||
navigateToMediaInfoFragment(item)
|
||||
})
|
||||
|
||||
(binding.itemsRecyclerView.adapter as ViewItemPagingAdapter).addLoadStateListener {
|
||||
when (it.refresh) {
|
||||
is LoadState.Error -> {
|
||||
val error = Exception((it.refresh as LoadState.Error).error)
|
||||
bindUiStateError(LibraryViewModel.UiState.Error(error))
|
||||
}
|
||||
is LoadState.Loading -> {
|
||||
bindUiStateLoading()
|
||||
}
|
||||
is LoadState.NotLoading -> {
|
||||
binding.loadingIndicator.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
|
@ -119,8 +135,14 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: LibraryViewModel.UiState.Normal) {
|
||||
val adapter = binding.itemsRecyclerView.adapter as ViewItemListAdapter
|
||||
adapter.submitList(uiState.items)
|
||||
val adapter = binding.itemsRecyclerView.adapter as ViewItemPagingAdapter
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
uiState.items.collect {
|
||||
adapter.submitData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.itemsRecyclerView.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package dev.jdtech.jellyfin.repository
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
class ItemsPagingSource(
|
||||
private val jellyfinApi: JellyfinApi,
|
||||
private val parentId: UUID?,
|
||||
private val includeTypes: List<BaseItemKind>?,
|
||||
private val recursive: Boolean,
|
||||
private val sortBy: SortBy,
|
||||
private val sortOrder: SortOrder
|
||||
) : PagingSource<Int, BaseItemDto>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, BaseItemDto> {
|
||||
val position = params.key ?: 0
|
||||
|
||||
Timber.d("Retrieving position: $position")
|
||||
|
||||
return try {
|
||||
val response = jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
parentId = parentId,
|
||||
includeItemTypes = includeTypes,
|
||||
recursive = recursive,
|
||||
sortBy = listOf(sortBy.SortString),
|
||||
sortOrder = listOf(sortOrder),
|
||||
startIndex = position,
|
||||
limit = params.loadSize
|
||||
).content.items.orEmpty()
|
||||
LoadResult.Page(
|
||||
data = response,
|
||||
prevKey = if (position == 0) null else position - params.loadSize,
|
||||
nextKey = if (response.isEmpty()) null else position + params.loadSize
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, BaseItemDto>): Int {
|
||||
return 0
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
package dev.jdtech.jellyfin.repository
|
||||
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jellyfin.sdk.model.api.*
|
||||
import java.util.*
|
||||
|
||||
|
@ -18,6 +20,14 @@ interface JellyfinRepository {
|
|||
sortOrder: SortOrder = SortOrder.ASCENDING
|
||||
): List<BaseItemDto>
|
||||
|
||||
suspend fun getItemsPaging(
|
||||
parentId: UUID? = null,
|
||||
includeTypes: List<BaseItemKind>? = null,
|
||||
recursive: Boolean = false,
|
||||
sortBy: SortBy = SortBy.defaultValue,
|
||||
sortOrder: SortOrder = SortOrder.ASCENDING
|
||||
): Flow<PagingData<BaseItemDto>>
|
||||
|
||||
suspend fun getPersonItems(
|
||||
personIds: List<UUID>,
|
||||
includeTypes: List<BaseItemKind>? = null,
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package dev.jdtech.jellyfin.repository
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.*
|
||||
import timber.log.Timber
|
||||
|
@ -34,6 +38,32 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
).content.items.orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun getItemsPaging(
|
||||
parentId: UUID?,
|
||||
includeTypes: List<BaseItemKind>?,
|
||||
recursive: Boolean,
|
||||
sortBy: SortBy,
|
||||
sortOrder: SortOrder
|
||||
): Flow<PagingData<BaseItemDto>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 10,
|
||||
maxSize = 100,
|
||||
enablePlaceholders = false
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
ItemsPagingSource(
|
||||
jellyfinApi,
|
||||
parentId,
|
||||
includeTypes,
|
||||
recursive,
|
||||
sortBy,
|
||||
sortOrder
|
||||
)
|
||||
}
|
||||
).flow
|
||||
}
|
||||
|
||||
override suspend fun getPersonItems(
|
||||
personIds: List<UUID>,
|
||||
includeTypes: List<BaseItemKind>?,
|
||||
|
@ -51,7 +81,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
filters = listOf(ItemFilter.IS_FAVORITE),
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE),
|
||||
includeItemTypes = listOf(
|
||||
BaseItemKind.MOVIE,
|
||||
BaseItemKind.SERIES,
|
||||
BaseItemKind.EPISODE
|
||||
),
|
||||
recursive = true
|
||||
).content.items.orEmpty()
|
||||
}
|
||||
|
@ -61,7 +95,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
searchTerm = searchQuery,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE),
|
||||
includeItemTypes = listOf(
|
||||
BaseItemKind.MOVIE,
|
||||
BaseItemKind.SERIES,
|
||||
BaseItemKind.EPISODE
|
||||
),
|
||||
recursive = true
|
||||
).content.items.orEmpty()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.PagingData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
@ -22,7 +24,7 @@ constructor(
|
|||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
sealed class UiState {
|
||||
data class Normal(val items: List<BaseItemDto>) : UiState()
|
||||
data class Normal(val items: Flow<PagingData<BaseItemDto>>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val error: Exception) : UiState()
|
||||
}
|
||||
|
@ -46,8 +48,9 @@ constructor(
|
|||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
val items = jellyfinRepository.getItems(
|
||||
parentId,
|
||||
|
||||
val items = jellyfinRepository.getItemsPaging(
|
||||
parentId = parentId,
|
||||
includeTypes = if (itemType != null) listOf(itemType) else null,
|
||||
recursive = true,
|
||||
sortBy = sortBy,
|
||||
|
|
Loading…
Reference in a new issue