refactor: rework RecyclerViews to set adapter once (#6971)

This commit is contained in:
Thomas W. 2025-01-29 16:02:45 +01:00 committed by GitHub
parent 87aca083a6
commit 0cf7abb07d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 483 additions and 328 deletions

View File

@ -2,21 +2,29 @@ package com.github.libretube.ui.activities
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.databinding.VideoTagRowBinding
import com.github.libretube.ui.viewholders.VideoTagsViewHolder
class VideoTagsAdapter(private val tags: List<String>) :
RecyclerView.Adapter<VideoTagsViewHolder>() {
class VideoTagsAdapter :
ListAdapter<String, VideoTagsViewHolder>(object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoTagsViewHolder {
val binding = VideoTagRowBinding.inflate(LayoutInflater.from(parent.context))
return VideoTagsViewHolder(binding)
}
override fun getItemCount() = tags.size
override fun onBindViewHolder(holder: VideoTagsViewHolder, position: Int) {
val tag = tags[position]
val tag = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
tagText.text = tag
root.setOnClickListener {

View File

@ -47,13 +47,16 @@ class WelcomeActivity : BaseActivity() {
val binding = ActivityWelcomeBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.instancesRecycler.layoutManager = LinearLayoutManager(this@WelcomeActivity)
val adapter = InstancesAdapter(viewModel.selectedInstanceIndex.value) { index ->
viewModel.selectedInstanceIndex.value = index
binding.okay.alpha = 1f
}
binding.instancesRecycler.adapter = adapter
// ALl the binding values are optional due to two different possible layouts (normal, landscape)
viewModel.instances.observe(this) { instances ->
binding.instancesRecycler.layoutManager = LinearLayoutManager(this@WelcomeActivity)
binding.instancesRecycler.adapter = InstancesAdapter(ImmutableList.copyOf(instances), viewModel.selectedInstanceIndex.value) { index ->
viewModel.selectedInstanceIndex.value = index
binding.okay.alpha = 1f
}
adapter.submitList(ImmutableList.copyOf(instances))
binding.progress.isGone = true
}
viewModel.fetchInstances()

View File

@ -2,25 +2,35 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.databinding.AddChannelToGroupRowBinding
import com.github.libretube.db.obj.SubscriptionGroup
import com.github.libretube.ui.viewholders.AddChannelToGroupViewHolder
class AddChannelToGroupAdapter(
private val channelGroups: MutableList<SubscriptionGroup>,
private val channelId: String
) : RecyclerView.Adapter<AddChannelToGroupViewHolder>() {
) : ListAdapter<SubscriptionGroup, AddChannelToGroupViewHolder>(object: DiffUtil.ItemCallback<SubscriptionGroup>() {
override fun areItemsTheSame(oldItem: SubscriptionGroup, newItem: SubscriptionGroup): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: SubscriptionGroup,
newItem: SubscriptionGroup
): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddChannelToGroupViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = AddChannelToGroupRowBinding.inflate(layoutInflater, parent, false)
return AddChannelToGroupViewHolder(binding)
}
override fun getItemCount() = channelGroups.size
override fun onBindViewHolder(holder: AddChannelToGroupViewHolder, position: Int) {
val channelGroup = channelGroups[position]
val channelGroup = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
groupName.text = channelGroup.name

View File

@ -9,7 +9,8 @@ import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.VideoRowBinding
@ -37,9 +38,21 @@ import kotlin.io.path.fileSize
class DownloadsAdapter(
private val context: Context,
private val downloadTab: DownloadTab,
private val downloads: MutableList<DownloadWithItems>,
private val toggleDownload: (DownloadWithItems) -> Boolean
) : RecyclerView.Adapter<DownloadsViewHolder>() {
) : ListAdapter<DownloadWithItems, DownloadsViewHolder>(object :
DiffUtil.ItemCallback<DownloadWithItems>() {
override fun areItemsTheSame(oldItem: DownloadWithItems, newItem: DownloadWithItems): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: DownloadWithItems,
newItem: DownloadWithItems
): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsViewHolder {
val binding = VideoRowBinding.inflate(
LayoutInflater.from(parent.context),
@ -51,8 +64,8 @@ class DownloadsAdapter(
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: DownloadsViewHolder, position: Int) {
val download = downloads[position].download
val items = downloads[position].downloadItems
val download = getItem(holder.bindingAdapterPosition).download
val items = getItem(holder.bindingAdapterPosition).downloadItems
holder.binding.apply {
fileSize.isVisible = true
@ -96,7 +109,7 @@ class DownloadsAdapter(
}
progressBar.setOnClickListener {
val isDownloading = toggleDownload(downloads[position])
val isDownloading = toggleDownload(getItem(holder.bindingAdapterPosition))
resumePauseBtn.setImageResource(
if (isDownloading) {
@ -113,7 +126,11 @@ class DownloadsAdapter(
intent.putExtra(IntentData.videoId, download.videoId)
root.context.startActivity(intent)
} else {
BackgroundHelper.playOnBackgroundOffline(root.context, download.videoId, downloadTab)
BackgroundHelper.playOnBackgroundOffline(
root.context,
download.videoId,
downloadTab
)
NavigationHelper.openAudioPlayerFragment(root.context, offlinePlayer = true)
}
}
@ -152,11 +169,9 @@ class DownloadsAdapter(
.show()
}
fun itemAt(index: Int) = downloads[index]
fun deleteDownload(position: Int) {
val download = downloads[position].download
val items = downloads[position].downloadItems
val download = getItem(position).download
val items = getItem(position).downloadItems
items.forEach {
it.path.deleteIfExists()
@ -168,9 +183,9 @@ class DownloadsAdapter(
runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().deleteDownload(download)
}
downloads.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
submitList(currentList.toMutableList().also {
it.removeAt(position)
})
}
fun restoreItem(position: Int) {
@ -178,6 +193,4 @@ class DownloadsAdapter(
notifyItemRemoved(position)
notifyItemInserted(position)
}
override fun getItemCount() = downloads.size
}

View File

@ -3,18 +3,26 @@ package com.github.libretube.ui.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.R
import com.github.libretube.api.obj.PipedInstance
import com.github.libretube.databinding.InstanceRowBinding
import com.github.libretube.ui.viewholders.InstancesViewHolder
import com.google.common.collect.ImmutableList
class InstancesAdapter(
private val instances: ImmutableList<PipedInstance>,
initialSelectionApiIndex: Int?,
private val onSelectInstance: (index: Int) -> Unit
) : RecyclerView.Adapter<InstancesViewHolder>() {
) : ListAdapter<PipedInstance, InstancesViewHolder>(object: DiffUtil.ItemCallback<PipedInstance>() {
override fun areItemsTheSame(oldItem: PipedInstance, newItem: PipedInstance): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: PipedInstance, newItem: PipedInstance): Boolean {
return oldItem == newItem
}
}) {
private var selectedInstanceIndex = initialSelectionApiIndex?.takeIf { it >= 0 }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstancesViewHolder {
@ -23,11 +31,9 @@ class InstancesAdapter(
return InstancesViewHolder(binding)
}
override fun getItemCount() = instances.size
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: InstancesViewHolder, position: Int) {
val instance = instances[position]
val instance = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
var instanceText = "${instance.name} ${instance.locations}"

View File

@ -3,7 +3,9 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.api.obj.Subscription
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.LegacySubscriptionChannelBinding
import com.github.libretube.extensions.toID
@ -13,9 +15,17 @@ import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.viewholders.LegacySubscriptionViewHolder
class LegacySubscriptionAdapter(
private val subscriptions: List<com.github.libretube.api.obj.Subscription>
) : RecyclerView.Adapter<LegacySubscriptionViewHolder>() {
class LegacySubscriptionAdapter : ListAdapter<Subscription, LegacySubscriptionViewHolder>(object :
DiffUtil.ItemCallback<Subscription>() {
override fun areItemsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(
parent: ViewGroup,
@ -27,7 +37,7 @@ class LegacySubscriptionAdapter(
}
override fun onBindViewHolder(holder: LegacySubscriptionViewHolder, position: Int) {
val subscription = subscriptions[position]
val subscription = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
channelName.text = subscription.name
ImageHelper.loadImage(
@ -51,6 +61,4 @@ class LegacySubscriptionAdapter(
}
}
}
override fun getItemCount() = subscriptions.size
}

View File

@ -5,7 +5,8 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.PlaylistBookmarkRowBinding
@ -23,9 +24,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class PlaylistBookmarkAdapter(
private val bookmarks: List<PlaylistBookmark>,
private val bookmarkMode: BookmarkMode = BookmarkMode.FRAGMENT
) : RecyclerView.Adapter<PlaylistBookmarkViewHolder>() {
) : ListAdapter<PlaylistBookmark, PlaylistBookmarkViewHolder>(object: DiffUtil.ItemCallback<PlaylistBookmark>() {
override fun areItemsTheSame(oldItem: PlaylistBookmark, newItem: PlaylistBookmark): Boolean {
return oldItem.playlistId == newItem.playlistId
}
override fun areContentsTheSame(oldItem: PlaylistBookmark, newItem: PlaylistBookmark): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistBookmarkViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when (bookmarkMode) {
@ -39,8 +48,6 @@ class PlaylistBookmarkAdapter(
}
}
override fun getItemCount() = bookmarks.size
private fun showPlaylistOptions(context: Context, bookmark: PlaylistBookmark) {
val sheet = PlaylistOptionsBottomSheet()
sheet.arguments = bundleOf(
@ -54,7 +61,7 @@ class PlaylistBookmarkAdapter(
}
override fun onBindViewHolder(holder: PlaylistBookmarkViewHolder, position: Int) {
val bookmark = bookmarks[position]
val bookmark = getItem(holder.bindingAdapterPosition)
holder.playlistBookmarkBinding?.apply {
ImageHelper.loadImage(bookmark.thumbnailUrl, thumbnail)
playlistName.text = bookmark.playlistName

View File

@ -3,7 +3,8 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.R
import com.github.libretube.api.obj.Playlists
import com.github.libretube.constants.IntentData
@ -17,18 +18,18 @@ import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet.Companion.PLAYL
import com.github.libretube.ui.viewholders.PlaylistsViewHolder
class PlaylistsAdapter(
private val playlists: MutableList<Playlists>,
private val playlistType: PlaylistType
) : RecyclerView.Adapter<PlaylistsViewHolder>() {
override fun getItemCount() = playlists.size
fun updateItems(newItems: List<Playlists>) {
val oldSize = playlists.size
playlists.addAll(newItems)
notifyItemRangeInserted(oldSize, playlists.size)
) : ListAdapter<Playlists, PlaylistsViewHolder>(object : DiffUtil.ItemCallback<Playlists>() {
override fun areItemsTheSame(oldItem: Playlists, newItem: Playlists): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Playlists, newItem: Playlists): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = PlaylistsRowBinding.inflate(layoutInflater, parent, false)
@ -36,7 +37,7 @@ class PlaylistsAdapter(
}
override fun onBindViewHolder(holder: PlaylistsViewHolder, position: Int) {
val playlist = playlists[position]
val playlist = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
// set imageview drawable as empty playlist if imageview empty
if (playlist.thumbnail.orEmpty().split("/").size <= 4) {
@ -80,7 +81,7 @@ class PlaylistsAdapter(
if (isPlaylistToBeDeleted) {
// try to refresh the playlists in the library on deletion success
onDelete(position, root.context as BaseActivity)
onDelete(position)
}
}
@ -99,11 +100,10 @@ class PlaylistsAdapter(
}
}
private fun onDelete(position: Int, activity: BaseActivity) {
playlists.removeAt(position)
activity.runOnUiThread {
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
private fun onDelete(position: Int) {
val newList = currentList.toMutableList().also {
it.removeAt(position)
}
submitList(newList)
}
}

View File

@ -2,9 +2,9 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.databinding.SuggestionRowBinding
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.SearchHistoryItem
@ -13,13 +13,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
class SearchHistoryAdapter(
private var historyList: List<String>,
private val searchView: SearchView
) :
RecyclerView.Adapter<SuggestionsViewHolder>() {
private val onRootClickListener: (String) -> Unit,
private val onArrowClickListener: (String) -> Unit,
) : ListAdapter<String, SuggestionsViewHolder>(object: DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun getItemCount() = historyList.size
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = SuggestionRowBinding.inflate(layoutInflater, parent, false)
@ -27,26 +32,28 @@ class SearchHistoryAdapter(
}
override fun onBindViewHolder(holder: SuggestionsViewHolder, position: Int) {
val historyQuery = historyList[position]
val historyQuery = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
suggestionText.text = historyQuery
deleteHistory.isVisible = true
deleteHistory.setOnClickListener {
historyList -= historyQuery
val updatedList = currentList.toMutableList().also {
it.remove(historyQuery)
}
runBlocking(Dispatchers.IO) {
Database.searchHistoryDao().delete(SearchHistoryItem(historyQuery))
}
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
submitList(updatedList)
}
root.setOnClickListener {
searchView.setQuery(historyQuery, true)
onRootClickListener(historyQuery)
}
arrow.setOnClickListener {
searchView.setQuery(historyQuery, false)
onArrowClickListener(historyQuery)
}
}
}

View File

@ -2,18 +2,24 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.databinding.SuggestionRowBinding
import com.github.libretube.ui.viewholders.SuggestionsViewHolder
class SearchSuggestionsAdapter(
private var suggestionsList: List<String>,
private val searchView: SearchView
) :
RecyclerView.Adapter<SuggestionsViewHolder>() {
private val onRootClickListener: (String) -> Unit,
private val onArrowClickListener: (String) -> Unit,
) : ListAdapter<String, SuggestionsViewHolder>(object: DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun getItemCount() = suggestionsList.size
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
@ -22,14 +28,14 @@ class SearchSuggestionsAdapter(
}
override fun onBindViewHolder(holder: SuggestionsViewHolder, position: Int) {
val suggestion = suggestionsList[position]
val suggestion = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
suggestionText.text = suggestion
root.setOnClickListener {
searchView.setQuery(suggestion, true)
onRootClickListener(suggestion)
}
arrow.setOnClickListener {
searchView.setQuery(suggestion, false)
onArrowClickListener(suggestion)
}
}
}

View File

@ -3,7 +3,8 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.api.obj.Subscription
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.ChannelSubscriptionRowBinding
@ -15,12 +16,20 @@ import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder
class SubscriptionChannelAdapter(
private val subscriptions: MutableList<Subscription>
) : RecyclerView.Adapter<SubscriptionChannelViewHolder>() {
class SubscriptionChannelAdapter : ListAdapter<Subscription, SubscriptionChannelViewHolder>(object :
DiffUtil.ItemCallback<Subscription>() {
override fun areItemsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
return oldItem == newItem
}
}) {
private var visibleCount = 20
override fun getItemCount() = minOf(visibleCount, subscriptions.size)
override fun getItemCount() = minOf(visibleCount, currentList.size)
override fun onCreateViewHolder(
parent: ViewGroup,
@ -33,13 +42,13 @@ class SubscriptionChannelAdapter(
fun updateItems() {
val oldSize = visibleCount
visibleCount += minOf(10, subscriptions.size - oldSize)
visibleCount += minOf(10, currentList.size - oldSize)
if (visibleCount == oldSize) return
notifyItemRangeInserted(oldSize, visibleCount)
}
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
val subscription = subscriptions[position]
val subscription = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
subscriptionChannelName.text = subscription.name

View File

@ -2,7 +2,8 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.api.obj.Subscription
import com.github.libretube.databinding.SubscriptionGroupChannelRowBinding
import com.github.libretube.db.obj.SubscriptionGroup
@ -12,10 +13,18 @@ import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.viewholders.SubscriptionGroupChannelRowViewHolder
class SubscriptionGroupChannelsAdapter(
private val channels: List<Subscription>,
private val group: SubscriptionGroup,
private val onGroupChanged: (SubscriptionGroup) -> Unit
) : RecyclerView.Adapter<SubscriptionGroupChannelRowViewHolder>() {
) : ListAdapter<Subscription, SubscriptionGroupChannelRowViewHolder>(object: DiffUtil.ItemCallback<Subscription>() {
override fun areItemsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Subscription, newItem: Subscription): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
@ -25,10 +34,8 @@ class SubscriptionGroupChannelsAdapter(
return SubscriptionGroupChannelRowViewHolder(binding)
}
override fun getItemCount() = channels.size
override fun onBindViewHolder(holder: SubscriptionGroupChannelRowViewHolder, position: Int) {
val channel = channels[position]
val channel = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
root.setOnClickListener {
NavigationHelper.navigateChannel(root.context, channel.url)

View File

@ -8,8 +8,9 @@ import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.IntentData
@ -36,29 +37,38 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class VideosAdapter(
private val streamItems: MutableList<StreamItem>,
private val forceMode: LayoutMode = LayoutMode.RESPECT_PREF
) : RecyclerView.Adapter<VideosViewHolder>() {
override fun getItemCount() = streamItems.size
) : ListAdapter<StreamItem, VideosViewHolder>(object: DiffUtil.ItemCallback<StreamItem>() {
override fun areItemsTheSame(oldItem: StreamItem, newItem: StreamItem): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: StreamItem, newItem: StreamItem): Boolean {
return oldItem == newItem
}
}) {
override fun getItemViewType(position: Int): Int {
return if (streamItems[position].type == CAUGHT_UP_STREAM_TYPE) CAUGHT_UP_TYPE else NORMAL_TYPE
return if (currentList[position].type == CAUGHT_UP_STREAM_TYPE) CAUGHT_UP_TYPE else NORMAL_TYPE
}
fun insertItems(newItems: List<StreamItem>) {
val feedSize = streamItems.size
streamItems.addAll(newItems)
notifyItemRangeInserted(feedSize, newItems.size)
val updatedList = currentList.toMutableList().also {
it.addAll(newItems)
}
submitList(updatedList)
}
fun removeItemById(videoId: String) {
val index = streamItems.indexOfFirst {
val index = currentList.indexOfFirst {
it.url?.toID() == videoId
}.takeIf { it > 0 } ?: return
streamItems.removeAt(index)
val updatedList = currentList.toMutableList().also {
it.removeAt(index)
}
notifyItemRemoved(index)
notifyItemRangeChanged(index, itemCount)
submitList(updatedList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideosViewHolder {
@ -90,7 +100,7 @@ class VideosAdapter(
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VideosViewHolder, position: Int) {
val video = streamItems[position]
val video = getItem(holder.bindingAdapterPosition)
val videoId = video.url.orEmpty().toID()
val context = (

View File

@ -5,7 +5,8 @@ import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.db.DatabaseHolder
@ -24,27 +25,34 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class WatchHistoryAdapter(
private val watchHistory: MutableList<WatchHistoryItem>
) :
RecyclerView.Adapter<WatchHistoryViewHolder>() {
class WatchHistoryAdapter : ListAdapter<WatchHistoryItem, WatchHistoryViewHolder>(object :
DiffUtil.ItemCallback<WatchHistoryItem>() {
override fun areItemsTheSame(oldItem: WatchHistoryItem, newItem: WatchHistoryItem): Boolean {
return oldItem == newItem
}
override fun getItemCount() = watchHistory.size
override fun areContentsTheSame(oldItem: WatchHistoryItem, newItem: WatchHistoryItem): Boolean {
return oldItem == newItem
}
}) {
fun removeFromWatchHistory(position: Int) {
val history = watchHistory[position]
val history = getItem(position)
runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.watchHistoryDao().delete(history)
}
watchHistory.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
val updatedList = currentList.toMutableList().also {
it.removeAt(position)
}
submitList(updatedList)
}
fun insertItems(items: List<WatchHistoryItem>) {
val oldSize = itemCount
this.watchHistory.addAll(items)
notifyItemRangeInserted(oldSize, itemCount)
val updatedList = currentList.toMutableList().also {
it.addAll(items)
}
submitList(updatedList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {
@ -54,7 +62,7 @@ class WatchHistoryAdapter(
}
override fun onBindViewHolder(holder: WatchHistoryViewHolder, position: Int) {
val video = watchHistory[position]
val video = getItem(holder.bindingAdapterPosition)
holder.binding.apply {
videoTitle.text = video.title
channelName.text = video.uploader

View File

@ -87,10 +87,8 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
}
nextPage = response.nextpage
val binding = _binding ?: return@launch
searchChannelAdapter = SearchChannelAdapter()
binding.channelRecView.adapter = searchChannelAdapter
searchChannelAdapter?.submitList(response.content)
val binding = _binding ?: return@launch
binding.progressBar.isGone = true
isLoading = false
@ -123,6 +121,9 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
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)
@ -147,9 +148,10 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
if (tabData?.data.isNullOrEmpty()) {
channelAdapter = VideosAdapter(
arguments.parcelableArrayList<StreamItem>(IntentData.videoList)!!,
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
)
).also {
it.submitList(arguments.parcelableArrayList<StreamItem>(IntentData.videoList)!!)
}
binding.channelRecView.adapter = channelAdapter
binding.progressBar.isGone = true

View File

@ -236,9 +236,10 @@ class ChannelFragment : DynamicLayoutManagerFragment(R.layout.fragment_channel)
}.attach()
channelAdapter = VideosAdapter(
response.relatedStreams.toMutableList(),
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
)
).also {
it.submitList(response.relatedStreams)
}
tabList.clear()
val tabs = listOf(ChannelTab(VIDEOS_TAB_KEY, "")) + response.tabs

View File

@ -146,6 +146,32 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentDownloadContentBinding.bind(view)
super.onViewCreated(view, savedInstanceState)
adapter = DownloadsAdapter(requireContext(), downloadTab) {
var isDownloading = false
val ids = it.downloadItems
.filter { item -> item.path.fileSize() < item.downloadSize }
.map { item -> item.id }
if (!serviceConnection.isBound) {
DownloadHelper.startDownloadService(requireContext())
bindDownloadService(ids.toIntArray())
return@DownloadsAdapter true
}
binder?.getService()?.let { service ->
isDownloading = ids.any { id -> service.isDownloading(id) }
ids.forEach { id ->
if (isDownloading) {
service.pause(id)
} else {
service.resume(id)
}
}
}
return@DownloadsAdapter isDownloading.not()
}
binding.downloadsRecView.adapter = adapter
var selectedSortType =
PreferenceHelper.getInt(PreferenceKeys.SELECTED_DOWNLOAD_SORT_TYPE, 0)
@ -156,7 +182,6 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
binding.sortType.text = filterOptions[index]
if (::adapter.isInitialized) {
sortDownloadList(index, selectedSortType)
adapter.notifyDataSetChanged()
}
selectedSortType = index
PreferenceHelper.putInt(
@ -178,32 +203,6 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
sortDownloadList(selectedSortType)
adapter = DownloadsAdapter(requireContext(), downloadTab, downloads) {
var isDownloading = false
val ids = it.downloadItems
.filter { item -> item.path.fileSize() < item.downloadSize }
.map { item -> item.id }
if (!serviceConnection.isBound) {
DownloadHelper.startDownloadService(requireContext())
bindDownloadService(ids.toIntArray())
return@DownloadsAdapter true
}
binder?.getService()?.let { service ->
isDownloading = ids.any { id -> service.isDownloading(id) }
ids.forEach { id ->
if (isDownloading) {
service.pause(id)
} else {
service.resume(id)
}
}
}
return@DownloadsAdapter isDownloading.not()
}
binding.downloadsRecView.adapter = adapter
binding.downloadsRecView.setOnDismissListener { position ->
adapter.showDeleteDialog(requireContext(), position)
@ -251,10 +250,10 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
private fun sortDownloadList(sortType: Int, previousSortType: Int? = null) {
if (previousSortType == null && sortType == 1) {
downloads.reverse()
adapter.submitList(downloads.reversed())
}
if (previousSortType != null && sortType != previousSortType) {
downloads.reverse()
adapter.submitList(downloads.reversed())
}
}
@ -269,7 +268,7 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
.setPositiveButton(R.string.okay) { _, _ ->
lifecycleScope.launch {
for (downloadIndex in downloads.size - 1 downTo 0) {
val download = adapter.itemAt(downloadIndex).download
val download = adapter.currentList[downloadIndex].download
if (!onlyDeleteWatchedVideos || DatabaseHelper.isVideoWatched(download.videoId, download.duration ?: 0)) {
adapter.deleteDownload(downloadIndex)
}

View File

@ -8,9 +8,6 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper
@ -41,10 +38,32 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
private val subscriptionsViewModel: SubscriptionsViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
private val trendingAdapter = VideosAdapter(forceMode = LayoutMode.TRENDING_ROW)
private val feedAdapter = VideosAdapter(forceMode = LayoutMode.RELATED_COLUMN)
private val watchingAdapter = VideosAdapter(forceMode = LayoutMode.RELATED_COLUMN)
private val bookmarkAdapter = PlaylistBookmarkAdapter(PlaylistBookmarkAdapter.Companion.BookmarkMode.HOME)
private val playlistAdapter = PlaylistsAdapter(playlistType = PlaylistsHelper.getPrivatePlaylistType())
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentHomeBinding.bind(view)
super.onViewCreated(view, savedInstanceState)
binding.trendingRV.adapter = trendingAdapter
binding.featuredRV.adapter = feedAdapter
binding.bookmarksRV.adapter = bookmarkAdapter
binding.playlistsRV.adapter = playlistAdapter
binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
if (itemCount == 0) {
binding.playlistsRV.isGone = true
binding.playlistsTV.isGone = true
}
}
})
binding.watchingRV.adapter = watchingAdapter
with(homeViewModel) {
trending.observe(viewLifecycleOwner, ::showTrending)
feed.observe(viewLifecycleOwner, ::showFeed)
@ -123,11 +142,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
if (streamItems == null) return
makeVisible(binding.trendingRV, binding.trendingTV)
binding.trendingRV.layoutManager = GridLayoutManager(context, 2)
binding.trendingRV.adapter = VideosAdapter(
streamItems.toMutableList(),
forceMode = LayoutMode.TRENDING_ROW
)
trendingAdapter.submitList(streamItems)
}
private fun showFeed(streamItems: List<StreamItem>?) {
@ -138,57 +153,29 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
val feedVideos = streamItems
.let { DatabaseHelper.filterByStatusAndWatchPosition(it, hideWatched) }
.take(20)
.toMutableList()
with(binding.featuredRV) {
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
adapter = VideosAdapter(feedVideos, forceMode = LayoutMode.RELATED_COLUMN)
}
feedAdapter.submitList(feedVideos)
}
private fun showBookmarks(bookmarks: List<PlaylistBookmark>?) {
if (bookmarks == null) return
makeVisible(binding.bookmarksTV, binding.bookmarksRV)
with(binding.bookmarksRV) {
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
adapter = PlaylistBookmarkAdapter(
bookmarks.toMutableList(),
PlaylistBookmarkAdapter.Companion.BookmarkMode.HOME
)
}
bookmarkAdapter.submitList(bookmarks)
}
private fun showPlaylists(playlists: List<Playlists>?) {
if (playlists == null) return
makeVisible(binding.playlistsRV, binding.playlistsTV)
binding.playlistsRV.layoutManager = LinearLayoutManager(context)
binding.playlistsRV.adapter = PlaylistsAdapter(
playlists.toMutableList(),
playlistType = PlaylistsHelper.getPrivatePlaylistType()
)
binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
if (itemCount == 0) {
binding.playlistsRV.isGone = true
binding.playlistsTV.isGone = true
}
}
})
playlistAdapter.submitList(playlists)
}
private fun showContinueWatching(unwatchedVideos: List<StreamItem>?) {
if (unwatchedVideos == null) return
makeVisible(binding.watchingRV, binding.watchingTV)
binding.watchingRV.layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
binding.watchingRV.adapter = VideosAdapter(
unwatchedVideos.toMutableList(),
forceMode = LayoutMode.RELATED_COLUMN
)
watchingAdapter.submitList(unwatchedVideos)
}
private fun updateLoading(isLoading: Boolean) {

View File

@ -45,6 +45,9 @@ class LibraryFragment : DynamicLayoutManagerFragment(R.layout.fragment_library)
private val commonPlayerViewModel: CommonPlayerViewModel by activityViewModels()
private val playlistsAdapter = PlaylistsAdapter(PlaylistsHelper.getPrivatePlaylistType())
private val playlistBookmarkAdapter = PlaylistBookmarkAdapter()
override fun setLayoutManagers(gridItems: Int) {
_binding?.bookmarksRecView?.layoutManager = GridLayoutManager(context, gridItems.ceilHalf())
_binding?.playlistRecView?.layoutManager = GridLayoutManager(context, gridItems.ceilHalf())
@ -54,6 +57,18 @@ class LibraryFragment : DynamicLayoutManagerFragment(R.layout.fragment_library)
_binding = FragmentLibraryBinding.bind(view)
super.onViewCreated(view, savedInstanceState)
binding.bookmarksRecView.adapter = playlistBookmarkAdapter
// listen for playlists to become deleted
playlistsAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
_binding?.nothingHere?.isVisible = playlistsAdapter.itemCount == 0
_binding?.sortTV?.isVisible = playlistsAdapter.itemCount > 0
super.onItemRangeRemoved(positionStart, itemCount)
}
})
binding.playlistRecView.adapter = playlistsAdapter
// listen for the mini player state changing
commonPlayerViewModel.isMiniPlayerVisible.observe(viewLifecycleOwner) {
updateFABMargin(it)
@ -142,7 +157,7 @@ class LibraryFragment : DynamicLayoutManagerFragment(R.layout.fragment_library)
binding.bookmarksCV.isVisible = bookmarks.isNotEmpty()
if (bookmarks.isNotEmpty()) {
binding.bookmarksRecView.adapter = PlaylistBookmarkAdapter(bookmarks)
playlistBookmarkAdapter.submitList(bookmarks)
}
}
}
@ -184,23 +199,8 @@ class LibraryFragment : DynamicLayoutManagerFragment(R.layout.fragment_library)
private fun showPlaylists(playlists: List<Playlists>) {
val binding = _binding ?: return
val playlistsAdapter = PlaylistsAdapter(
playlists.toMutableList(),
PlaylistsHelper.getPrivatePlaylistType()
)
// listen for playlists to become deleted
playlistsAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
_binding?.nothingHere?.isVisible = playlistsAdapter.itemCount == 0
_binding?.sortTV?.isVisible = playlistsAdapter.itemCount > 0
super.onItemRangeRemoved(positionStart, itemCount)
}
})
binding.nothingHere.isGone = true
binding.sortTV.isVisible = true
binding.playlistRecView.adapter = playlistsAdapter
playlistsAdapter.submitList(playlists)
}
}

View File

@ -1128,13 +1128,14 @@ class PlayerFragment : Fragment(R.layout.fragment_player), OnlinePlayerOptions {
if (PlayerHelper.relatedStreamsEnabled) {
val relatedLayoutManager = binding.relatedRecView.layoutManager as LinearLayoutManager
binding.relatedRecView.adapter = VideosAdapter(
streams.relatedStreams.filter { !it.title.isNullOrBlank() }.toMutableList(),
forceMode = if (relatedLayoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
VideosAdapter.Companion.LayoutMode.RELATED_COLUMN
} else {
VideosAdapter.Companion.LayoutMode.TRENDING_ROW
}
)
).also { adapter ->
adapter.submitList(streams.relatedStreams.filter { !it.title.isNullOrBlank() })
}
}
// update the subscribed state

View File

@ -325,6 +325,7 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
playlistId,
playlistType
)
// TODO make sure the adapter is set once in onViewCreated
binding.playlistRecView.adapter = playlistAdapter
// listen for playlist items to become deleted

View File

@ -7,9 +7,10 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData
@ -34,6 +35,27 @@ class SearchSuggestionsFragment : Fragment(R.layout.fragment_search_suggestions)
private val viewModel: SearchViewModel by activityViewModels()
private val mainActivity get() = activity as MainActivity
private val historyAdapter = SearchHistoryAdapter(
onRootClickListener = { historyQuery ->
runCatching {
(activity as MainActivity?)?.searchView
}.getOrNull()?.setQuery(historyQuery, true)
},
onArrowClickListener = { historyQuery ->
runCatching {
(activity as MainActivity?)?.searchView
}.getOrNull()?.setQuery(historyQuery, false)
}
)
private val suggestionsAdapter = SearchSuggestionsAdapter(
onRootClickListener = { suggestion ->
(activity as MainActivity?)?.searchView?.setQuery(suggestion, true)
},
onArrowClickListener = { suggestion ->
(activity as MainActivity?)?.searchView?.setQuery(suggestion, false)
},
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.searchQuery.value = arguments?.getString(IntentData.query)
@ -43,9 +65,15 @@ class SearchSuggestionsFragment : Fragment(R.layout.fragment_search_suggestions)
_binding = FragmentSearchSuggestionsBinding.bind(view)
super.onViewCreated(view, savedInstanceState)
binding.suggestionsRecycler.layoutManager = LinearLayoutManager(requireContext()).apply {
reverseLayout = true
stackFromEnd = true
viewModel.searchQuery
.map { it.isNullOrEmpty() }
.distinctUntilChanged()
.observe(viewLifecycleOwner) { isQueryEmpty ->
if (isQueryEmpty) {
binding.suggestionsRecycler.adapter = historyAdapter
} else if (PreferenceHelper.getBoolean(PreferenceKeys.SEARCH_SUGGESTIONS, true)) {
binding.suggestionsRecycler.adapter = suggestionsAdapter
}
}
// waiting for the query to change
@ -81,30 +109,19 @@ class SearchSuggestionsFragment : Fragment(R.layout.fragment_search_suggestions)
return@launch
}
// only load the suggestions if the input field didn't get cleared yet
val suggestionsAdapter = SearchSuggestionsAdapter(
response.reversed(),
(activity as MainActivity).searchView
)
if (isAdded && !viewModel.searchQuery.value.isNullOrEmpty()) {
binding.suggestionsRecycler.adapter = suggestionsAdapter
if (!viewModel.searchQuery.value.isNullOrEmpty()) {
suggestionsAdapter.submitList(response.reversed())
}
}
}
private fun showHistory() {
val searchView = runCatching {
(activity as MainActivity).searchView
}.getOrNull()
lifecycleScope.launch {
val historyList = withContext(Dispatchers.IO) {
Database.searchHistoryDao().getAll().map { it.query }
}
if (historyList.isNotEmpty() && searchView != null) {
binding.suggestionsRecycler.adapter = SearchHistoryAdapter(
historyList,
searchView
)
if (historyList.isNotEmpty()) {
historyAdapter.submitList(historyList)
} else {
binding.suggestionsRecycler.isGone = true
binding.historyEmpty.isVisible = true

View File

@ -64,10 +64,9 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
private var isCurrentTabSubChannels = false
private var isAppBarFullyExpanded = true
private var feedAdapter: VideosAdapter? = null
private var feedAdapter = VideosAdapter()
private val sortedFeed: MutableList<StreamItem> = mutableListOf()
private var channelsAdapter: SubscriptionChannelAdapter? = null
private var selectedSortOrder = PreferenceHelper.getInt(PreferenceKeys.FEED_SORT_ORDER, 0)
set(value) {
PreferenceHelper.putInt(PreferenceKeys.FEED_SORT_ORDER, value)
@ -84,6 +83,9 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
private var subChannelsRecyclerViewState: Parcelable? = null
private var subFeedRecyclerViewState: Parcelable? = null
private val legacySubscriptionsAdapter = LegacySubscriptionAdapter()
private val channelsAdapter = SubscriptionChannelAdapter()
override fun setLayoutManagers(gridItems: Int) {
_binding?.subFeed?.layoutManager = VideosAdapter.getLayout(requireContext(), gridItems)
}
@ -94,6 +96,27 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
setupSortAndFilter()
binding.subFeed.adapter = feedAdapter
val legacySubscriptions = PreferenceHelper.getBoolean(
PreferenceKeys.LEGACY_SUBSCRIPTIONS,
false
)
if (legacySubscriptions) {
binding.subChannels.layoutManager = GridLayoutManager(
context,
PreferenceHelper.getString(
PreferenceKeys.LEGACY_SUBSCRIPTIONS_COLUMNS,
"4"
).toInt()
)
binding.subChannels.adapter = legacySubscriptionsAdapter
} else {
binding.subChannels.layoutManager = LinearLayoutManager(context)
binding.subChannels.adapter = channelsAdapter
}
// Check if the AppBarLayout is fully expanded
binding.subscriptionsAppBar.addOnOffsetChangedListener { _, verticalOffset ->
isAppBarFullyExpanded = verticalOffset == 0
@ -145,7 +168,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
if (viewModel.subscriptions.value != null && isCurrentTabSubChannels) {
binding.subRefresh.isRefreshing = true
channelsAdapter?.updateItems()
channelsAdapter.updateItems()
binding.subRefresh.isRefreshing = false
}
}
@ -204,8 +227,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
private fun loadNextFeedItems() {
val binding = _binding ?: return
val feedAdapter = feedAdapter ?: return
val hasMore = sortedFeed.size > feedAdapter.itemCount
if (viewModel.videoFeed.value != null && !isCurrentTabSubChannels && !binding.subRefresh.isRefreshing && hasMore) {
binding.subRefresh.isRefreshing = true
@ -373,11 +394,8 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
val notLoaded = viewModel.videoFeed.value.isNullOrEmpty()
binding.subFeed.isGone = notLoaded
binding.emptyFeed.isVisible = notLoaded
feedAdapter = VideosAdapter(mutableListOf())
loadNextFeedItems()
binding.subFeed.adapter = feedAdapter
binding.toggleSubs.text = getString(R.string.subscriptions)
PreferenceHelper.updateLastFeedWatchedTime()
@ -393,18 +411,9 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
)
if (legacySubscriptions) {
binding.subChannels.layoutManager = GridLayoutManager(
context,
PreferenceHelper.getString(
PreferenceKeys.LEGACY_SUBSCRIPTIONS_COLUMNS,
"4"
).toInt()
)
binding.subChannels.adapter = LegacySubscriptionAdapter(subscriptions)
legacySubscriptionsAdapter.submitList(subscriptions)
} else {
binding.subChannels.layoutManager = LinearLayoutManager(context)
channelsAdapter = SubscriptionChannelAdapter(subscriptions.toMutableList())
binding.subChannels.adapter = channelsAdapter
channelsAdapter.submitList(subscriptions)
}
binding.subRefresh.isRefreshing = false
@ -420,7 +429,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
}
fun removeItem(videoId: String) {
feedAdapter?.removeItemById(videoId)
feedAdapter.removeItemById(videoId)
sortedFeed.removeAll { it.url?.toID() != videoId }
}

View File

@ -30,15 +30,18 @@ class TrendsFragment : DynamicLayoutManagerFragment(R.layout.fragment_trends) {
_binding = FragmentTrendsBinding.bind(view)
super.onViewCreated(view, savedInstanceState)
val adapter = VideosAdapter()
binding.recview.adapter = adapter
binding.recview.layoutManager?.onRestoreInstanceState(viewModel.recyclerViewState)
viewModel.trendingVideos.observe(viewLifecycleOwner) { videos ->
if (videos == null) return@observe
binding.recview.adapter = VideosAdapter(videos.toMutableList())
binding.recview.layoutManager?.onRestoreInstanceState(viewModel.recyclerViewState)
binding.homeRefresh.isRefreshing = false
binding.progressBar.isGone = true
adapter.submitList(videos)
if (videos.isEmpty()) {
Snackbar.make(binding.root, R.string.change_region, Snackbar.LENGTH_LONG)
.setAction(R.string.settings) {

View File

@ -50,6 +50,8 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
private var isLoading = false
private var recyclerViewState: Parcelable? = null
private val watchHistoryAdapter = WatchHistoryAdapter()
private var selectedStatusFilter = PreferenceHelper.getInt(
PreferenceKeys.SELECTED_HISTORY_STATUS_FILTER,
0
@ -80,6 +82,31 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
_binding?.watchHistoryRecView?.updatePadding(bottom = if (it) 64f.dpToPx() else 0)
}
binding.watchHistoryRecView.setOnDismissListener { position ->
watchHistoryAdapter.removeFromWatchHistory(position)
}
// observe changes to indicate if the history is empty
watchHistoryAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
if (watchHistoryAdapter.itemCount == 0) {
binding.historyContainer.isGone = true
binding.historyEmpty.isVisible = true
}
}
})
binding.watchHistoryRecView.adapter = watchHistoryAdapter
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
binding.watchHistoryRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
recyclerViewState = binding.watchHistoryRecView.layoutManager?.onSaveInstanceState()
}
})
lifecycleScope.launch {
val history = withContext(Dispatchers.IO) {
DatabaseHelper.getWatchHistoryPage(1, HISTORY_PAGE_SIZE)
@ -139,14 +166,6 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
}.show(childFragmentManager)
}
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
binding.watchHistoryRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
recyclerViewState = binding.watchHistoryRecView.layoutManager?.onSaveInstanceState()
}
})
showWatchHistory(history)
}
@ -157,7 +176,6 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
private fun showWatchHistory(history: List<WatchHistoryItem>) {
val watchHistory = history.filterByStatusAndWatchPosition()
val watchHistoryAdapter = WatchHistoryAdapter(watchHistory.toMutableList())
binding.playAll.setOnClickListener {
PlayingQueue.resetToDefaults()
@ -170,26 +188,10 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
keepQueue = true
)
}
binding.watchHistoryRecView.adapter = watchHistoryAdapter
watchHistoryAdapter.submitList(history)
binding.historyEmpty.isGone = true
binding.historyContainer.isVisible = true
binding.watchHistoryRecView.setOnDismissListener { position ->
watchHistoryAdapter.removeFromWatchHistory(position)
}
// observe changes to indicate if the history is empty
watchHistoryAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
if (watchHistoryAdapter.itemCount == 0) {
binding.historyContainer.isGone = true
binding.historyEmpty.isVisible = true
}
}
})
// add a listener for scroll end, delay needed to prevent loading new ones the first time
handler.postDelayed(200) {
if (_binding == null) return@postDelayed

View File

@ -187,8 +187,10 @@ class InstanceSettings : BasePreferenceFragment() {
binding.optionsRecycler.layoutManager = LinearLayoutManager(context)
val instances = ImmutableList.copyOf(this.instances)
binding.optionsRecycler.adapter = InstancesAdapter(instances, selectedIndex) {
binding.optionsRecycler.adapter = InstancesAdapter(selectedIndex) {
selectedInstance = instances[it].apiUrl
}.also {
it.submitList(instances)
}
MaterialAlertDialogBuilder(requireContext())

View File

@ -3,7 +3,6 @@ package com.github.libretube.ui.sheets
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DialogAddChannelToGroupBinding
@ -16,6 +15,10 @@ import kotlinx.coroutines.withContext
class AddChannelToGroupSheet : ExpandedBottomSheet(R.layout.dialog_add_channel_to_group) {
private lateinit var channelId: String
private val addToGroupAdapter by lazy(LazyThreadSafetyMode.NONE) {
AddChannelToGroupAdapter(channelId)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -26,7 +29,8 @@ class AddChannelToGroupSheet : ExpandedBottomSheet(R.layout.dialog_add_channel_t
super.onViewCreated(view, savedInstanceState)
val binding = DialogAddChannelToGroupBinding.bind(view)
binding.groupsRV.layoutManager = LinearLayoutManager(context)
binding.groupsRV.adapter = addToGroupAdapter
binding.cancel.setOnClickListener {
requireDialog().dismiss()
}
@ -36,7 +40,7 @@ class AddChannelToGroupSheet : ExpandedBottomSheet(R.layout.dialog_add_channel_t
val subscriptionGroups = subGroupsDao.getAll().sortedBy { it.index }.toMutableList()
withContext(Dispatchers.Main) {
binding.groupsRV.adapter = AddChannelToGroupAdapter(subscriptionGroups, channelId)
addToGroupAdapter.submitList(subscriptionGroups)
binding.okay.setOnClickListener {
requireDialog().hide()

View File

@ -30,8 +30,16 @@ class EditChannelGroupSheet : ExpandedBottomSheet(R.layout.dialog_edit_channel_g
private val channelGroupsModel: EditChannelGroupsModel by activityViewModels()
private var channels = listOf<Subscription>()
private val channelsAdapter = SubscriptionGroupChannelsAdapter(
channelGroupsModel.groupToEdit!!
) {
channelGroupsModel.groupToEdit = it
updateConfirmStatus()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = DialogEditChannelGroupBinding.bind(view)
binding.channelsRV.adapter = channelsAdapter
binding.groupName.setText(channelGroupsModel.groupToEdit?.name)
val oldGroupName = channelGroupsModel.groupToEdit?.name.orEmpty()
@ -97,15 +105,12 @@ class EditChannelGroupSheet : ExpandedBottomSheet(R.layout.dialog_edit_channel_g
}
private fun showChannels(channels: List<Subscription>, query: String?) {
binding.channelsRV.adapter = SubscriptionGroupChannelsAdapter(
channels.filter { query == null || it.name.lowercase().contains(query.lowercase()) },
channelGroupsModel.groupToEdit!!
) {
channelGroupsModel.groupToEdit = it
updateConfirmStatus()
}
binding.subscriptionsContainer.isVisible = true
binding.progress.isVisible = false
channelsAdapter.submitList(
channels.filter { query == null || it.name.lowercase().contains(query.lowercase()) }
)
}
private fun updateConfirmStatus() {

View File

@ -10,7 +10,6 @@ import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.text.parseAsHtml
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
@ -33,6 +32,8 @@ class DescriptionLayout(
private var streams: Streams? = null
var handleLink: (link: String) -> Unit = {}
private val videoTagsAdapter = VideoTagsAdapter()
init {
binding.playerTitleLayout.setOnClickListener {
toggleDescription()
@ -41,6 +42,8 @@ class DescriptionLayout(
streams?.title?.let { ClipboardHelper.save(context, text = it) }
true
}
binding.tagsRecycler.adapter = videoTagsAdapter
}
fun setSegments(segments: List<Segment>) {
@ -101,9 +104,7 @@ class DescriptionLayout(
"${context?.getString(R.string.visibility)}: $visibility"
if (streams.tags.isNotEmpty()) {
binding.tagsRecycler.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
binding.tagsRecycler.adapter = VideoTagsAdapter(streams.tags)
videoTagsAdapter.submitList(streams.tags)
}
binding.tagsRecycler.isVisible = streams.tags.isNotEmpty()

View File

@ -20,8 +20,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:visibility="gone"
android:text="Sponsor"/>
android:text="Sponsor"
android:visibility="gone" />
</LinearLayout>
@ -133,7 +133,9 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tags_recycler"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -48,7 +49,9 @@
android:id="@+id/groupsRV"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
android:layout_weight="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/add_channel_to_group_row" />
<LinearLayout
android:layout_width="wrap_content"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -35,10 +36,12 @@
android:id="@+id/featuredRV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
android:orientation="horizontal"
android:paddingHorizontal="10dp"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<TextView
android:id="@+id/watchingTV"
@ -49,10 +52,12 @@
android:id="@+id/watchingRV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
android:orientation="horizontal"
android:paddingHorizontal="10dp"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<TextView
android:id="@+id/trendingTV"
@ -70,7 +75,9 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2" />
</RelativeLayout>
@ -84,7 +91,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
android:orientation="horizontal"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<TextView
android:id="@+id/playlistsTV"
@ -96,7 +105,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
@ -134,8 +144,8 @@
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:textSize="12sp"
android:text="@string/retry"/>
android:text="@string/retry"
android:textSize="12sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/change_instance"
@ -144,8 +154,8 @@
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_marginTop="5dp"
android:textSize="12sp"
android:text="@string/change_instance"/>
android:text="@string/change_instance"
android:textSize="12sp" />
</LinearLayout>
</FrameLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -9,7 +10,10 @@
android:id="@+id/suggestions_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="10dp" />
android:layout_marginVertical="10dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:reverseLayout="true"
app:stackFromEnd="true" />
<LinearLayout
android:id="@+id/history_empty"