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"
|
val aboutLibrariesVersion = "10.3.0"
|
||||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibrariesVersion")
|
implementation("com.mikepenz:aboutlibraries-core:$aboutLibrariesVersion")
|
||||||
implementation("com.mikepenz:aboutlibraries:$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.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.paging.LoadState
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
|
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.databinding.FragmentLibraryBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
||||||
|
@ -91,10 +92,25 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.itemsRecyclerView.adapter =
|
binding.itemsRecyclerView.adapter =
|
||||||
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
|
ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item ->
|
||||||
navigateToMediaInfoFragment(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.lifecycleScope.launch {
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
|
@ -119,8 +135,14 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindUiStateNormal(uiState: LibraryViewModel.UiState.Normal) {
|
private fun bindUiStateNormal(uiState: LibraryViewModel.UiState.Normal) {
|
||||||
val adapter = binding.itemsRecyclerView.adapter as ViewItemListAdapter
|
val adapter = binding.itemsRecyclerView.adapter as ViewItemPagingAdapter
|
||||||
adapter.submitList(uiState.items)
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
uiState.items.collect {
|
||||||
|
adapter.submitData(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
binding.loadingIndicator.isVisible = false
|
binding.loadingIndicator.isVisible = false
|
||||||
binding.itemsRecyclerView.isVisible = true
|
binding.itemsRecyclerView.isVisible = true
|
||||||
binding.errorLayout.errorPanel.isVisible = false
|
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
|
package dev.jdtech.jellyfin.repository
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.paging.PagingData
|
||||||
import dev.jdtech.jellyfin.utils.SortBy
|
import dev.jdtech.jellyfin.utils.SortBy
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.jellyfin.sdk.model.api.*
|
import org.jellyfin.sdk.model.api.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -18,6 +20,14 @@ interface JellyfinRepository {
|
||||||
sortOrder: SortOrder = SortOrder.ASCENDING
|
sortOrder: SortOrder = SortOrder.ASCENDING
|
||||||
): List<BaseItemDto>
|
): 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(
|
suspend fun getPersonItems(
|
||||||
personIds: List<UUID>,
|
personIds: List<UUID>,
|
||||||
includeTypes: List<BaseItemKind>? = null,
|
includeTypes: List<BaseItemKind>? = null,
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package dev.jdtech.jellyfin.repository
|
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.api.JellyfinApi
|
||||||
import dev.jdtech.jellyfin.utils.SortBy
|
import dev.jdtech.jellyfin.utils.SortBy
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.*
|
import org.jellyfin.sdk.model.api.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -34,6 +38,32 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
).content.items.orEmpty()
|
).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(
|
override suspend fun getPersonItems(
|
||||||
personIds: List<UUID>,
|
personIds: List<UUID>,
|
||||||
includeTypes: List<BaseItemKind>?,
|
includeTypes: List<BaseItemKind>?,
|
||||||
|
@ -51,7 +81,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
jellyfinApi.itemsApi.getItems(
|
jellyfinApi.itemsApi.getItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
filters = listOf(ItemFilter.IS_FAVORITE),
|
filters = listOf(ItemFilter.IS_FAVORITE),
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE),
|
includeItemTypes = listOf(
|
||||||
|
BaseItemKind.MOVIE,
|
||||||
|
BaseItemKind.SERIES,
|
||||||
|
BaseItemKind.EPISODE
|
||||||
|
),
|
||||||
recursive = true
|
recursive = true
|
||||||
).content.items.orEmpty()
|
).content.items.orEmpty()
|
||||||
}
|
}
|
||||||
|
@ -61,7 +95,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
jellyfinApi.itemsApi.getItems(
|
jellyfinApi.itemsApi.getItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
searchTerm = searchQuery,
|
searchTerm = searchQuery,
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE),
|
includeItemTypes = listOf(
|
||||||
|
BaseItemKind.MOVIE,
|
||||||
|
BaseItemKind.SERIES,
|
||||||
|
BaseItemKind.EPISODE
|
||||||
|
),
|
||||||
recursive = true
|
recursive = true
|
||||||
).content.items.orEmpty()
|
).content.items.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
|
import androidx.paging.PagingData
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import dev.jdtech.jellyfin.utils.SortBy
|
import dev.jdtech.jellyfin.utils.SortBy
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
@ -22,7 +24,7 @@ constructor(
|
||||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
|
|
||||||
sealed class UiState {
|
sealed class UiState {
|
||||||
data class Normal(val items: List<BaseItemDto>) : UiState()
|
data class Normal(val items: Flow<PagingData<BaseItemDto>>) : UiState()
|
||||||
object Loading : UiState()
|
object Loading : UiState()
|
||||||
data class Error(val error: Exception) : UiState()
|
data class Error(val error: Exception) : UiState()
|
||||||
}
|
}
|
||||||
|
@ -46,8 +48,9 @@ constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiState.emit(UiState.Loading)
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getItems(
|
|
||||||
parentId,
|
val items = jellyfinRepository.getItemsPaging(
|
||||||
|
parentId = parentId,
|
||||||
includeTypes = if (itemType != null) listOf(itemType) else null,
|
includeTypes = if (itemType != null) listOf(itemType) else null,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
sortBy = sortBy,
|
sortBy = sortBy,
|
||||||
|
|
Loading…
Reference in a new issue