mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 23:40:33 +05:30
refactor: use paging adapter for channel content items
This commit is contained in:
parent
2c73287d71
commit
4cde38504a
@ -1,176 +0,0 @@
|
||||
package com.github.libretube.ui.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.JsonHelper
|
||||
import com.github.libretube.api.obj.ContentItem
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.databinding.ChannelRowBinding
|
||||
import com.github.libretube.databinding.PlaylistsRowBinding
|
||||
import com.github.libretube.databinding.VideoRowBinding
|
||||
import com.github.libretube.enums.PlaylistType
|
||||
import com.github.libretube.extensions.formatShort
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.helpers.ImageHelper
|
||||
import com.github.libretube.helpers.NavigationHelper
|
||||
import com.github.libretube.ui.adapters.callbacks.DiffUtilItemCallback
|
||||
import com.github.libretube.ui.base.BaseActivity
|
||||
import com.github.libretube.ui.extensions.setFormattedDuration
|
||||
import com.github.libretube.ui.extensions.setWatchProgressLength
|
||||
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||
import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
|
||||
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
|
||||
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
|
||||
import com.github.libretube.ui.viewholders.SearchViewHolder
|
||||
import com.github.libretube.util.TextUtils
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
// TODO: Replace with SearchResultsAdapter when migrating the channel fragment to use Paging as well
|
||||
class SearchChannelAdapter : ListAdapter<ContentItem, SearchViewHolder>(
|
||||
DiffUtilItemCallback(
|
||||
areItemsTheSame = { oldItem, newItem -> oldItem.url == newItem.url },
|
||||
areContentsTheSame = { _, _ -> true },
|
||||
)
|
||||
) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
|
||||
return when (viewType) {
|
||||
0 -> SearchViewHolder(VideoRowBinding.inflate(layoutInflater, parent, false))
|
||||
1 -> SearchViewHolder(ChannelRowBinding.inflate(layoutInflater, parent, false))
|
||||
2 -> SearchViewHolder(PlaylistsRowBinding.inflate(layoutInflater, parent, false))
|
||||
else -> throw IllegalArgumentException("Invalid type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
|
||||
val searchItem = currentList[position]
|
||||
|
||||
val videoRowBinding = holder.videoRowBinding
|
||||
val channelRowBinding = holder.channelRowBinding
|
||||
val playlistRowBinding = holder.playlistRowBinding
|
||||
|
||||
if (videoRowBinding != null) {
|
||||
bindVideo(searchItem, videoRowBinding, position)
|
||||
} else if (channelRowBinding != null) {
|
||||
bindChannel(searchItem, channelRowBinding)
|
||||
} else if (playlistRowBinding != null) {
|
||||
bindPlaylist(searchItem, playlistRowBinding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (currentList[position].type) {
|
||||
StreamItem.TYPE_STREAM -> 0
|
||||
StreamItem.TYPE_CHANNEL -> 1
|
||||
StreamItem.TYPE_PLAYLIST -> 2
|
||||
else -> 3
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindVideo(item: ContentItem, binding: VideoRowBinding, position: Int) {
|
||||
binding.apply {
|
||||
ImageHelper.loadImage(item.thumbnail, thumbnail)
|
||||
thumbnailDuration.setFormattedDuration(item.duration, item.isShort, item.uploaded)
|
||||
videoTitle.text = item.title
|
||||
videoInfo.text = TextUtils.formatViewsString(root.context, item.views, item.uploaded)
|
||||
|
||||
channelContainer.isGone = true
|
||||
|
||||
root.setOnClickListener {
|
||||
NavigationHelper.navigateVideo(root.context, item.url)
|
||||
}
|
||||
|
||||
val videoId = item.url.toID()
|
||||
val activity = (root.context as BaseActivity)
|
||||
val fragmentManager = activity.supportFragmentManager
|
||||
root.setOnLongClickListener {
|
||||
fragmentManager.setFragmentResultListener(
|
||||
VideoOptionsBottomSheet.VIDEO_OPTIONS_SHEET_REQUEST_KEY,
|
||||
activity
|
||||
) { _, _ ->
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
val sheet = VideoOptionsBottomSheet()
|
||||
val contentItemString = JsonHelper.json.encodeToString(item)
|
||||
val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString)
|
||||
sheet.arguments = bundleOf(IntentData.streamItem to streamItem)
|
||||
sheet.show(fragmentManager, SearchChannelAdapter::class.java.name)
|
||||
true
|
||||
}
|
||||
channelContainer.setOnClickListener {
|
||||
NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
|
||||
}
|
||||
watchProgress.setWatchProgressLength(videoId, item.duration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindChannel(item: ContentItem, binding: ChannelRowBinding) {
|
||||
binding.apply {
|
||||
ImageHelper.loadImage(item.thumbnail, searchChannelImage, true)
|
||||
searchChannelName.text = item.name
|
||||
|
||||
val subscribers = item.subscribers.formatShort()
|
||||
searchViews.text = if (item.subscribers >= 0 && item.videos >= 0) {
|
||||
root.context.getString(R.string.subscriberAndVideoCounts, subscribers, item.videos)
|
||||
} else if (item.subscribers >= 0) {
|
||||
root.context.getString(R.string.subscribers, subscribers)
|
||||
} else if (item.videos >= 0) {
|
||||
root.context.getString(R.string.videoCount, item.videos)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
root.setOnClickListener {
|
||||
NavigationHelper.navigateChannel(root.context, item.url)
|
||||
}
|
||||
|
||||
var subscribed = false
|
||||
binding.searchSubButton.setupSubscriptionButton(item.url.toID(), item.name?.toID()) {
|
||||
subscribed = it
|
||||
}
|
||||
|
||||
root.setOnLongClickListener {
|
||||
val channelOptionsSheet = ChannelOptionsBottomSheet()
|
||||
channelOptionsSheet.arguments = bundleOf(
|
||||
IntentData.channelId to item.url.toID(),
|
||||
IntentData.channelName to item.name,
|
||||
IntentData.isSubscribed to subscribed
|
||||
)
|
||||
channelOptionsSheet.show((root.context as BaseActivity).supportFragmentManager)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPlaylist(item: ContentItem, binding: PlaylistsRowBinding) {
|
||||
binding.apply {
|
||||
ImageHelper.loadImage(item.thumbnail, playlistThumbnail)
|
||||
if (item.videos >= 0) videoCount.text = item.videos.toString()
|
||||
playlistTitle.text = item.name
|
||||
playlistDescription.text = item.uploaderName
|
||||
root.setOnClickListener {
|
||||
NavigationHelper.navigatePlaylist(root.context, item.url, PlaylistType.PUBLIC)
|
||||
}
|
||||
|
||||
root.setOnLongClickListener {
|
||||
val sheet = PlaylistOptionsBottomSheet()
|
||||
sheet.arguments = bundleOf(
|
||||
IntentData.playlistId to item.url.toID(),
|
||||
IntentData.playlistName to item.name.orEmpty(),
|
||||
IntentData.playlistType to PlaylistType.PUBLIC
|
||||
)
|
||||
sheet.show(
|
||||
(root.context as BaseActivity).supportFragmentManager,
|
||||
PlaylistOptionsBottomSheet::class.java.name
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package com.github.libretube.ui.adapters
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import com.github.libretube.R
|
||||
@ -91,10 +92,12 @@ class SearchResultsAdapter(
|
||||
private fun bindVideo(item: ContentItem, binding: VideoRowBinding, position: Int) {
|
||||
binding.apply {
|
||||
ImageHelper.loadImage(item.thumbnail, thumbnail)
|
||||
|
||||
thumbnailDuration.setFormattedDuration(item.duration, item.isShort, item.uploaded)
|
||||
videoTitle.text = item.title
|
||||
videoInfo.text = TextUtils.formatViewsString(root.context, item.views, item.uploaded)
|
||||
|
||||
channelContainer.isGone = item.uploaderAvatar.isNullOrEmpty()
|
||||
channelName.text = item.uploaderName
|
||||
ImageHelper.loadImage(item.uploaderAvatar, channelImage, true)
|
||||
|
||||
@ -184,12 +187,10 @@ class SearchResultsAdapter(
|
||||
}
|
||||
|
||||
root.setOnLongClickListener {
|
||||
val playlistId = item.url.toID()
|
||||
val playlistName = item.name!!
|
||||
val sheet = PlaylistOptionsBottomSheet()
|
||||
sheet.arguments = bundleOf(
|
||||
IntentData.playlistId to playlistId,
|
||||
IntentData.playlistName to playlistName,
|
||||
IntentData.playlistId to item.url.toID(),
|
||||
IntentData.playlistName to item.name.orEmpty(),
|
||||
IntentData.playlistType to PlaylistType.PUBLIC
|
||||
)
|
||||
sheet.show(
|
||||
|
@ -3,26 +3,27 @@ package com.github.libretube.ui.fragments
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.ChannelTab
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.databinding.FragmentChannelContentBinding
|
||||
import com.github.libretube.extensions.TAG
|
||||
import com.github.libretube.extensions.ceilHalf
|
||||
import com.github.libretube.extensions.parcelable
|
||||
import com.github.libretube.extensions.parcelableArrayList
|
||||
import com.github.libretube.ui.adapters.SearchChannelAdapter
|
||||
import com.github.libretube.ui.adapters.SearchResultsAdapter
|
||||
import com.github.libretube.ui.adapters.VideosAdapter
|
||||
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
||||
import com.github.libretube.ui.extensions.addOnBottomReachedListener
|
||||
import com.github.libretube.ui.models.sources.ChannelTabPagingSource
|
||||
import com.github.libretube.util.deArrow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -31,12 +32,7 @@ import kotlinx.coroutines.withContext
|
||||
class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_channel_content) {
|
||||
private var _binding: FragmentChannelContentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var channelId: String? = null
|
||||
private var searchChannelAdapter: SearchChannelAdapter? = null
|
||||
private var channelAdapter: VideosAdapter? = null
|
||||
private var recyclerViewState: Parcelable? = null
|
||||
private var nextPage: String? = null
|
||||
private var isLoading: Boolean = false
|
||||
|
||||
override fun setLayoutManagers(gridItems: Int) {
|
||||
binding.channelRecView.layoutManager = GridLayoutManager(
|
||||
@ -45,109 +41,26 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchChannelNextPage(nextPage: String): String? {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
MediaServiceRepository.instance.getChannelNextPage(channelId!!, nextPage).apply {
|
||||
relatedStreams = relatedStreams.deArrow()
|
||||
}
|
||||
}
|
||||
channelAdapter?.insertItems(response.relatedStreams)
|
||||
return response.nextpage
|
||||
}
|
||||
|
||||
private suspend fun fetchTabNextPage(nextPage: String, tab: ChannelTab): String? {
|
||||
val newContent = withContext(Dispatchers.IO) {
|
||||
MediaServiceRepository.instance.getChannelTab(tab.data, nextPage)
|
||||
}.apply {
|
||||
content = content.deArrow()
|
||||
}
|
||||
|
||||
searchChannelAdapter?.let {
|
||||
it.submitList(it.currentList + newContent.content)
|
||||
}
|
||||
return newContent.nextpage
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
|
||||
binding.channelRecView.layoutManager?.onRestoreInstanceState(recyclerViewState)
|
||||
}
|
||||
|
||||
private fun loadChannelTab(tab: ChannelTab) = lifecycleScope.launch {
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
MediaServiceRepository.instance.getChannelTab(tab.data)
|
||||
}.apply {
|
||||
content = content.deArrow()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_binding?.progressBar?.isGone = true
|
||||
return@launch
|
||||
}
|
||||
nextPage = response.nextpage
|
||||
|
||||
searchChannelAdapter?.submitList(response.content)
|
||||
val binding = _binding ?: return@launch
|
||||
binding.progressBar.isGone = true
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private fun loadNextPage(isVideo: Boolean, tab: ChannelTab) {
|
||||
if (isLoading) return
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
isLoading = true
|
||||
nextPage = if (isVideo) {
|
||||
fetchChannelNextPage(nextPage ?: return@launch)
|
||||
} else {
|
||||
fetchTabNextPage(nextPage ?: return@launch, tab)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG(), e.toString())
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
_binding = FragmentChannelContentBinding.bind(view)
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val arguments = requireArguments()
|
||||
val channelId = arguments.getString(IntentData.channelId)!!
|
||||
|
||||
val tabData = arguments.parcelable<ChannelTab>(IntentData.tabData)
|
||||
channelId = arguments.getString(IntentData.channelId)
|
||||
nextPage = arguments.getString(IntentData.nextPage)
|
||||
|
||||
searchChannelAdapter = SearchChannelAdapter()
|
||||
binding.channelRecView.adapter = searchChannelAdapter
|
||||
|
||||
binding.channelRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
recyclerViewState = binding.channelRecView.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
if (_binding == null || isLoading) return
|
||||
|
||||
val visibleItemCount = recyclerView.layoutManager!!.childCount
|
||||
val totalItemCount = recyclerView.layoutManager!!.getItemCount()
|
||||
val firstVisibleItemPosition =
|
||||
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
|
||||
if (firstVisibleItemPosition + visibleItemCount >= totalItemCount) {
|
||||
loadNextPage(tabData?.data!!.isEmpty(), tabData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (tabData?.data.isNullOrEmpty()) {
|
||||
channelAdapter = VideosAdapter(
|
||||
var nextPage = arguments.getString(IntentData.nextPage)
|
||||
var isLoading = false
|
||||
|
||||
val channelAdapter = VideosAdapter(
|
||||
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
|
||||
).also {
|
||||
it.submitList(arguments.parcelableArrayList<StreamItem>(IntentData.videoList)!!)
|
||||
@ -155,8 +68,52 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
||||
binding.channelRecView.adapter = channelAdapter
|
||||
binding.progressBar.isGone = true
|
||||
|
||||
binding.channelRecView.addOnBottomReachedListener {
|
||||
if (isLoading || nextPage == null) return@addOnBottomReachedListener
|
||||
|
||||
isLoading = true
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val resp = try {
|
||||
MediaServiceRepository.instance.getChannelNextPage(channelId, nextPage!!).apply {
|
||||
relatedStreams = relatedStreams.deArrow()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
nextPage = resp.nextpage
|
||||
withContext(Dispatchers.Main) {
|
||||
channelAdapter.insertItems(resp.relatedStreams)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadChannelTab(tabData ?: return)
|
||||
val searchChannelAdapter = SearchResultsAdapter()
|
||||
binding.channelRecView.adapter = searchChannelAdapter
|
||||
|
||||
val pagingFlow = Pager(
|
||||
PagingConfig(pageSize = 20, enablePlaceholders = false),
|
||||
pagingSourceFactory = { ChannelTabPagingSource(tabData!!) }
|
||||
).flow
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
launch {
|
||||
pagingFlow.collect {
|
||||
searchChannelAdapter.submitData(it)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
searchChannelAdapter.loadStateFlow.collect {
|
||||
if (it.refresh is LoadState.NotLoading) {
|
||||
binding.progressBar.isGone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
package com.github.libretube.ui.models.sources
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.ChannelTab
|
||||
import com.github.libretube.api.obj.ContentItem
|
||||
import com.github.libretube.util.deArrow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ChannelTabPagingSource(
|
||||
private val tab: ChannelTab
|
||||
): PagingSource<String, ContentItem>() {
|
||||
override fun getRefreshKey(state: PagingState<String, ContentItem>) = null
|
||||
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, ContentItem> {
|
||||
return try {
|
||||
val resp = withContext(Dispatchers.IO) {
|
||||
MediaServiceRepository.instance.getChannelTab(tab.data, params.key).apply {
|
||||
content = content.deArrow()
|
||||
}
|
||||
}
|
||||
LoadResult.Page(resp.content, null, resp.nextpage)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user