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.LayoutInflater
import android.view.ViewGroup 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.databinding.VideoTagRowBinding
import com.github.libretube.ui.viewholders.VideoTagsViewHolder import com.github.libretube.ui.viewholders.VideoTagsViewHolder
class VideoTagsAdapter(private val tags: List<String>) : class VideoTagsAdapter :
RecyclerView.Adapter<VideoTagsViewHolder>() { 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoTagsViewHolder {
val binding = VideoTagRowBinding.inflate(LayoutInflater.from(parent.context)) val binding = VideoTagRowBinding.inflate(LayoutInflater.from(parent.context))
return VideoTagsViewHolder(binding) return VideoTagsViewHolder(binding)
} }
override fun getItemCount() = tags.size
override fun onBindViewHolder(holder: VideoTagsViewHolder, position: Int) { override fun onBindViewHolder(holder: VideoTagsViewHolder, position: Int) {
val tag = tags[position] val tag = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
tagText.text = tag tagText.text = tag
root.setOnClickListener { root.setOnClickListener {

View File

@ -47,13 +47,16 @@ class WelcomeActivity : BaseActivity() {
val binding = ActivityWelcomeBinding.inflate(layoutInflater) val binding = ActivityWelcomeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// 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.layoutManager = LinearLayoutManager(this@WelcomeActivity)
binding.instancesRecycler.adapter = InstancesAdapter(ImmutableList.copyOf(instances), viewModel.selectedInstanceIndex.value) { index -> val adapter = InstancesAdapter(viewModel.selectedInstanceIndex.value) { index ->
viewModel.selectedInstanceIndex.value = index viewModel.selectedInstanceIndex.value = index
binding.okay.alpha = 1f 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 ->
adapter.submitList(ImmutableList.copyOf(instances))
binding.progress.isGone = true binding.progress.isGone = true
} }
viewModel.fetchInstances() viewModel.fetchInstances()

View File

@ -2,25 +2,35 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup 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.databinding.AddChannelToGroupRowBinding
import com.github.libretube.db.obj.SubscriptionGroup import com.github.libretube.db.obj.SubscriptionGroup
import com.github.libretube.ui.viewholders.AddChannelToGroupViewHolder import com.github.libretube.ui.viewholders.AddChannelToGroupViewHolder
class AddChannelToGroupAdapter( class AddChannelToGroupAdapter(
private val channelGroups: MutableList<SubscriptionGroup>,
private val channelId: String 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddChannelToGroupViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
val binding = AddChannelToGroupRowBinding.inflate(layoutInflater, parent, false) val binding = AddChannelToGroupRowBinding.inflate(layoutInflater, parent, false)
return AddChannelToGroupViewHolder(binding) return AddChannelToGroupViewHolder(binding)
} }
override fun getItemCount() = channelGroups.size
override fun onBindViewHolder(holder: AddChannelToGroupViewHolder, position: Int) { override fun onBindViewHolder(holder: AddChannelToGroupViewHolder, position: Int) {
val channelGroup = channelGroups[position] val channelGroup = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
groupName.text = channelGroup.name groupName.text = channelGroup.name

View File

@ -9,7 +9,8 @@ import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible 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.R
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.VideoRowBinding import com.github.libretube.databinding.VideoRowBinding
@ -37,9 +38,21 @@ import kotlin.io.path.fileSize
class DownloadsAdapter( class DownloadsAdapter(
private val context: Context, private val context: Context,
private val downloadTab: DownloadTab, private val downloadTab: DownloadTab,
private val downloads: MutableList<DownloadWithItems>,
private val toggleDownload: (DownloadWithItems) -> Boolean 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsViewHolder {
val binding = VideoRowBinding.inflate( val binding = VideoRowBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
@ -51,8 +64,8 @@ class DownloadsAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: DownloadsViewHolder, position: Int) { override fun onBindViewHolder(holder: DownloadsViewHolder, position: Int) {
val download = downloads[position].download val download = getItem(holder.bindingAdapterPosition).download
val items = downloads[position].downloadItems val items = getItem(holder.bindingAdapterPosition).downloadItems
holder.binding.apply { holder.binding.apply {
fileSize.isVisible = true fileSize.isVisible = true
@ -96,7 +109,7 @@ class DownloadsAdapter(
} }
progressBar.setOnClickListener { progressBar.setOnClickListener {
val isDownloading = toggleDownload(downloads[position]) val isDownloading = toggleDownload(getItem(holder.bindingAdapterPosition))
resumePauseBtn.setImageResource( resumePauseBtn.setImageResource(
if (isDownloading) { if (isDownloading) {
@ -113,7 +126,11 @@ class DownloadsAdapter(
intent.putExtra(IntentData.videoId, download.videoId) intent.putExtra(IntentData.videoId, download.videoId)
root.context.startActivity(intent) root.context.startActivity(intent)
} else { } else {
BackgroundHelper.playOnBackgroundOffline(root.context, download.videoId, downloadTab) BackgroundHelper.playOnBackgroundOffline(
root.context,
download.videoId,
downloadTab
)
NavigationHelper.openAudioPlayerFragment(root.context, offlinePlayer = true) NavigationHelper.openAudioPlayerFragment(root.context, offlinePlayer = true)
} }
} }
@ -152,11 +169,9 @@ class DownloadsAdapter(
.show() .show()
} }
fun itemAt(index: Int) = downloads[index]
fun deleteDownload(position: Int) { fun deleteDownload(position: Int) {
val download = downloads[position].download val download = getItem(position).download
val items = downloads[position].downloadItems val items = getItem(position).downloadItems
items.forEach { items.forEach {
it.path.deleteIfExists() it.path.deleteIfExists()
@ -168,9 +183,9 @@ class DownloadsAdapter(
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().deleteDownload(download) DatabaseHolder.Database.downloadDao().deleteDownload(download)
} }
downloads.removeAt(position) submitList(currentList.toMutableList().also {
notifyItemRemoved(position) it.removeAt(position)
notifyItemRangeChanged(position, itemCount) })
} }
fun restoreItem(position: Int) { fun restoreItem(position: Int) {
@ -178,6 +193,4 @@ class DownloadsAdapter(
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemInserted(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.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup 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.R
import com.github.libretube.api.obj.PipedInstance import com.github.libretube.api.obj.PipedInstance
import com.github.libretube.databinding.InstanceRowBinding import com.github.libretube.databinding.InstanceRowBinding
import com.github.libretube.ui.viewholders.InstancesViewHolder import com.github.libretube.ui.viewholders.InstancesViewHolder
import com.google.common.collect.ImmutableList
class InstancesAdapter( class InstancesAdapter(
private val instances: ImmutableList<PipedInstance>,
initialSelectionApiIndex: Int?, initialSelectionApiIndex: Int?,
private val onSelectInstance: (index: Int) -> Unit 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 } private var selectedInstanceIndex = initialSelectionApiIndex?.takeIf { it >= 0 }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstancesViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InstancesViewHolder {
@ -23,11 +31,9 @@ class InstancesAdapter(
return InstancesViewHolder(binding) return InstancesViewHolder(binding)
} }
override fun getItemCount() = instances.size
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: InstancesViewHolder, position: Int) { override fun onBindViewHolder(holder: InstancesViewHolder, position: Int) {
val instance = instances[position] val instance = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
var instanceText = "${instance.name} ${instance.locations}" 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf 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.constants.IntentData
import com.github.libretube.databinding.LegacySubscriptionChannelBinding import com.github.libretube.databinding.LegacySubscriptionChannelBinding
import com.github.libretube.extensions.toID 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.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.viewholders.LegacySubscriptionViewHolder import com.github.libretube.ui.viewholders.LegacySubscriptionViewHolder
class LegacySubscriptionAdapter( class LegacySubscriptionAdapter : ListAdapter<Subscription, LegacySubscriptionViewHolder>(object :
private val subscriptions: List<com.github.libretube.api.obj.Subscription> DiffUtil.ItemCallback<Subscription>() {
) : RecyclerView.Adapter<LegacySubscriptionViewHolder>() { 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( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
@ -27,7 +37,7 @@ class LegacySubscriptionAdapter(
} }
override fun onBindViewHolder(holder: LegacySubscriptionViewHolder, position: Int) { override fun onBindViewHolder(holder: LegacySubscriptionViewHolder, position: Int) {
val subscription = subscriptions[position] val subscription = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
channelName.text = subscription.name channelName.text = subscription.name
ImageHelper.loadImage( 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 android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible 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.R
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.PlaylistBookmarkRowBinding import com.github.libretube.databinding.PlaylistBookmarkRowBinding
@ -23,9 +24,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PlaylistBookmarkAdapter( class PlaylistBookmarkAdapter(
private val bookmarks: List<PlaylistBookmark>,
private val bookmarkMode: BookmarkMode = BookmarkMode.FRAGMENT 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistBookmarkViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
return when (bookmarkMode) { return when (bookmarkMode) {
@ -39,8 +48,6 @@ class PlaylistBookmarkAdapter(
} }
} }
override fun getItemCount() = bookmarks.size
private fun showPlaylistOptions(context: Context, bookmark: PlaylistBookmark) { private fun showPlaylistOptions(context: Context, bookmark: PlaylistBookmark) {
val sheet = PlaylistOptionsBottomSheet() val sheet = PlaylistOptionsBottomSheet()
sheet.arguments = bundleOf( sheet.arguments = bundleOf(
@ -54,7 +61,7 @@ class PlaylistBookmarkAdapter(
} }
override fun onBindViewHolder(holder: PlaylistBookmarkViewHolder, position: Int) { override fun onBindViewHolder(holder: PlaylistBookmarkViewHolder, position: Int) {
val bookmark = bookmarks[position] val bookmark = getItem(holder.bindingAdapterPosition)
holder.playlistBookmarkBinding?.apply { holder.playlistBookmarkBinding?.apply {
ImageHelper.loadImage(bookmark.thumbnailUrl, thumbnail) ImageHelper.loadImage(bookmark.thumbnailUrl, thumbnail)
playlistName.text = bookmark.playlistName playlistName.text = bookmark.playlistName

View File

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

View File

@ -2,9 +2,9 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible 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.databinding.SuggestionRowBinding
import com.github.libretube.db.DatabaseHolder.Database import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.SearchHistoryItem import com.github.libretube.db.obj.SearchHistoryItem
@ -13,13 +13,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class SearchHistoryAdapter( class SearchHistoryAdapter(
private var historyList: List<String>, private val onRootClickListener: (String) -> Unit,
private val searchView: SearchView private val onArrowClickListener: (String) -> Unit,
) : ) : ListAdapter<String, SuggestionsViewHolder>(object: DiffUtil.ItemCallback<String>() {
RecyclerView.Adapter<SuggestionsViewHolder>() { 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
val binding = SuggestionRowBinding.inflate(layoutInflater, parent, false) val binding = SuggestionRowBinding.inflate(layoutInflater, parent, false)
@ -27,26 +32,28 @@ class SearchHistoryAdapter(
} }
override fun onBindViewHolder(holder: SuggestionsViewHolder, position: Int) { override fun onBindViewHolder(holder: SuggestionsViewHolder, position: Int) {
val historyQuery = historyList[position] val historyQuery = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
suggestionText.text = historyQuery suggestionText.text = historyQuery
deleteHistory.isVisible = true deleteHistory.isVisible = true
deleteHistory.setOnClickListener { deleteHistory.setOnClickListener {
historyList -= historyQuery val updatedList = currentList.toMutableList().also {
it.remove(historyQuery)
}
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
Database.searchHistoryDao().delete(SearchHistoryItem(historyQuery)) Database.searchHistoryDao().delete(SearchHistoryItem(historyQuery))
} }
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount) submitList(updatedList)
} }
root.setOnClickListener { root.setOnClickListener {
searchView.setQuery(historyQuery, true) onRootClickListener(historyQuery)
} }
arrow.setOnClickListener { 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.databinding.SuggestionRowBinding import com.github.libretube.databinding.SuggestionRowBinding
import com.github.libretube.ui.viewholders.SuggestionsViewHolder import com.github.libretube.ui.viewholders.SuggestionsViewHolder
class SearchSuggestionsAdapter( class SearchSuggestionsAdapter(
private var suggestionsList: List<String>, private val onRootClickListener: (String) -> Unit,
private val searchView: SearchView private val onArrowClickListener: (String) -> Unit,
) : ) : ListAdapter<String, SuggestionsViewHolder>(object: DiffUtil.ItemCallback<String>() {
RecyclerView.Adapter<SuggestionsViewHolder>() { 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
@ -22,14 +28,14 @@ class SearchSuggestionsAdapter(
} }
override fun onBindViewHolder(holder: SuggestionsViewHolder, position: Int) { override fun onBindViewHolder(holder: SuggestionsViewHolder, position: Int) {
val suggestion = suggestionsList[position] val suggestion = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
suggestionText.text = suggestion suggestionText.text = suggestion
root.setOnClickListener { root.setOnClickListener {
searchView.setQuery(suggestion, true) onRootClickListener(suggestion)
} }
arrow.setOnClickListener { 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf 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.api.obj.Subscription
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.ChannelSubscriptionRowBinding 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.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder
class SubscriptionChannelAdapter( class SubscriptionChannelAdapter : ListAdapter<Subscription, SubscriptionChannelViewHolder>(object :
private val subscriptions: MutableList<Subscription> DiffUtil.ItemCallback<Subscription>() {
) : RecyclerView.Adapter<SubscriptionChannelViewHolder>() { 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 private var visibleCount = 20
override fun getItemCount() = minOf(visibleCount, subscriptions.size) override fun getItemCount() = minOf(visibleCount, currentList.size)
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
@ -33,13 +42,13 @@ class SubscriptionChannelAdapter(
fun updateItems() { fun updateItems() {
val oldSize = visibleCount val oldSize = visibleCount
visibleCount += minOf(10, subscriptions.size - oldSize) visibleCount += minOf(10, currentList.size - oldSize)
if (visibleCount == oldSize) return if (visibleCount == oldSize) return
notifyItemRangeInserted(oldSize, visibleCount) notifyItemRangeInserted(oldSize, visibleCount)
} }
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) { override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
val subscription = subscriptions[position] val subscription = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
subscriptionChannelName.text = subscription.name subscriptionChannelName.text = subscription.name

View File

@ -2,7 +2,8 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup 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.api.obj.Subscription
import com.github.libretube.databinding.SubscriptionGroupChannelRowBinding import com.github.libretube.databinding.SubscriptionGroupChannelRowBinding
import com.github.libretube.db.obj.SubscriptionGroup import com.github.libretube.db.obj.SubscriptionGroup
@ -12,10 +13,18 @@ import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.viewholders.SubscriptionGroupChannelRowViewHolder import com.github.libretube.ui.viewholders.SubscriptionGroupChannelRowViewHolder
class SubscriptionGroupChannelsAdapter( class SubscriptionGroupChannelsAdapter(
private val channels: List<Subscription>,
private val group: SubscriptionGroup, private val group: SubscriptionGroup,
private val onGroupChanged: (SubscriptionGroup) -> Unit 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( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
@ -25,10 +34,8 @@ class SubscriptionGroupChannelsAdapter(
return SubscriptionGroupChannelRowViewHolder(binding) return SubscriptionGroupChannelRowViewHolder(binding)
} }
override fun getItemCount() = channels.size
override fun onBindViewHolder(holder: SubscriptionGroupChannelRowViewHolder, position: Int) { override fun onBindViewHolder(holder: SubscriptionGroupChannelRowViewHolder, position: Int) {
val channel = channels[position] val channel = getItem(holder.bindingAdapterPosition)
holder.binding.apply { holder.binding.apply {
root.setOnClickListener { root.setOnClickListener {
NavigationHelper.navigateChannel(root.context, channel.url) 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.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.LayoutManager import androidx.recyclerview.widget.RecyclerView.LayoutManager
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
@ -36,29 +37,38 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class VideosAdapter( class VideosAdapter(
private val streamItems: MutableList<StreamItem>,
private val forceMode: LayoutMode = LayoutMode.RESPECT_PREF private val forceMode: LayoutMode = LayoutMode.RESPECT_PREF
) : RecyclerView.Adapter<VideosViewHolder>() { ) : ListAdapter<StreamItem, VideosViewHolder>(object: DiffUtil.ItemCallback<StreamItem>() {
override fun getItemCount() = streamItems.size 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 { 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>) { fun insertItems(newItems: List<StreamItem>) {
val feedSize = streamItems.size val updatedList = currentList.toMutableList().also {
streamItems.addAll(newItems) it.addAll(newItems)
notifyItemRangeInserted(feedSize, newItems.size) }
submitList(updatedList)
} }
fun removeItemById(videoId: String) { fun removeItemById(videoId: String) {
val index = streamItems.indexOfFirst { val index = currentList.indexOfFirst {
it.url?.toID() == videoId it.url?.toID() == videoId
}.takeIf { it > 0 } ?: return }.takeIf { it > 0 } ?: return
streamItems.removeAt(index) val updatedList = currentList.toMutableList().also {
it.removeAt(index)
}
notifyItemRemoved(index) submitList(updatedList)
notifyItemRangeChanged(index, itemCount)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideosViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideosViewHolder {
@ -90,7 +100,7 @@ class VideosAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VideosViewHolder, position: Int) { override fun onBindViewHolder(holder: VideosViewHolder, position: Int) {
val video = streamItems[position] val video = getItem(holder.bindingAdapterPosition)
val videoId = video.url.orEmpty().toID() val videoId = video.url.orEmpty().toID()
val context = ( val context = (

View File

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

View File

@ -87,10 +87,8 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
} }
nextPage = response.nextpage nextPage = response.nextpage
val binding = _binding ?: return@launch
searchChannelAdapter = SearchChannelAdapter()
binding.channelRecView.adapter = searchChannelAdapter
searchChannelAdapter?.submitList(response.content) searchChannelAdapter?.submitList(response.content)
val binding = _binding ?: return@launch
binding.progressBar.isGone = true binding.progressBar.isGone = true
isLoading = false isLoading = false
@ -123,6 +121,9 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
channelId = arguments.getString(IntentData.channelId) channelId = arguments.getString(IntentData.channelId)
nextPage = arguments.getString(IntentData.nextPage) nextPage = arguments.getString(IntentData.nextPage)
searchChannelAdapter = SearchChannelAdapter()
binding.channelRecView.adapter = searchChannelAdapter
binding.channelRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.channelRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState) super.onScrollStateChanged(recyclerView, newState)
@ -147,9 +148,10 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
if (tabData?.data.isNullOrEmpty()) { if (tabData?.data.isNullOrEmpty()) {
channelAdapter = VideosAdapter( channelAdapter = VideosAdapter(
arguments.parcelableArrayList<StreamItem>(IntentData.videoList)!!,
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
) ).also {
it.submitList(arguments.parcelableArrayList<StreamItem>(IntentData.videoList)!!)
}
binding.channelRecView.adapter = channelAdapter binding.channelRecView.adapter = channelAdapter
binding.progressBar.isGone = true binding.progressBar.isGone = true

View File

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

View File

@ -146,39 +146,7 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentDownloadContentBinding.bind(view) _binding = FragmentDownloadContentBinding.bind(view)
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = DownloadsAdapter(requireContext(), downloadTab) {
var selectedSortType =
PreferenceHelper.getInt(PreferenceKeys.SELECTED_DOWNLOAD_SORT_TYPE, 0)
val filterOptions = resources.getStringArray(R.array.downloadSortOptions)
binding.sortType.text = filterOptions[selectedSortType]
binding.sortType.setOnClickListener {
BaseBottomSheet().setSimpleItems(filterOptions.toList()) { index ->
binding.sortType.text = filterOptions[index]
if (::adapter.isInitialized) {
sortDownloadList(index, selectedSortType)
adapter.notifyDataSetChanged()
}
selectedSortType = index
PreferenceHelper.putInt(
PreferenceKeys.SELECTED_DOWNLOAD_SORT_TYPE,
index
)
}.show(childFragmentManager)
}
lifecycleScope.launch {
val dbDownloads = withContext(Dispatchers.IO) {
Database.downloadDao().getAll()
}
downloads.clear()
downloads.addAll(dbDownloads.filterByTab(downloadTab))
if (downloads.isEmpty()) return@launch
sortDownloadList(selectedSortType)
adapter = DownloadsAdapter(requireContext(), downloadTab, downloads) {
var isDownloading = false var isDownloading = false
val ids = it.downloadItems val ids = it.downloadItems
.filter { item -> item.path.fileSize() < item.downloadSize } .filter { item -> item.path.fileSize() < item.downloadSize }
@ -205,6 +173,37 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
} }
binding.downloadsRecView.adapter = adapter binding.downloadsRecView.adapter = adapter
var selectedSortType =
PreferenceHelper.getInt(PreferenceKeys.SELECTED_DOWNLOAD_SORT_TYPE, 0)
val filterOptions = resources.getStringArray(R.array.downloadSortOptions)
binding.sortType.text = filterOptions[selectedSortType]
binding.sortType.setOnClickListener {
BaseBottomSheet().setSimpleItems(filterOptions.toList()) { index ->
binding.sortType.text = filterOptions[index]
if (::adapter.isInitialized) {
sortDownloadList(index, selectedSortType)
}
selectedSortType = index
PreferenceHelper.putInt(
PreferenceKeys.SELECTED_DOWNLOAD_SORT_TYPE,
index
)
}.show(childFragmentManager)
}
lifecycleScope.launch {
val dbDownloads = withContext(Dispatchers.IO) {
Database.downloadDao().getAll()
}
downloads.clear()
downloads.addAll(dbDownloads.filterByTab(downloadTab))
if (downloads.isEmpty()) return@launch
sortDownloadList(selectedSortType)
binding.downloadsRecView.setOnDismissListener { position -> binding.downloadsRecView.setOnDismissListener { position ->
adapter.showDeleteDialog(requireContext(), position) adapter.showDeleteDialog(requireContext(), position)
// put the item back to the center, as it's currently out of the screen // put the item back to the center, as it's currently out of the screen
@ -251,10 +250,10 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
private fun sortDownloadList(sortType: Int, previousSortType: Int? = null) { private fun sortDownloadList(sortType: Int, previousSortType: Int? = null) {
if (previousSortType == null && sortType == 1) { if (previousSortType == null && sortType == 1) {
downloads.reverse() adapter.submitList(downloads.reversed())
} }
if (previousSortType != null && sortType != previousSortType) { 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) { _, _ -> .setPositiveButton(R.string.okay) { _, _ ->
lifecycleScope.launch { lifecycleScope.launch {
for (downloadIndex in downloads.size - 1 downTo 0) { 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)) { if (!onlyDeleteWatchedVideos || DatabaseHelper.isVideoWatched(download.videoId, download.duration ?: 0)) {
adapter.deleteDownload(downloadIndex) adapter.deleteDownload(downloadIndex)
} }

View File

@ -8,9 +8,6 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController 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 androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper 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 subscriptionsViewModel: SubscriptionsViewModel by activityViewModels()
private val homeViewModel: HomeViewModel 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentHomeBinding.bind(view) _binding = FragmentHomeBinding.bind(view)
super.onViewCreated(view, savedInstanceState) 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) { with(homeViewModel) {
trending.observe(viewLifecycleOwner, ::showTrending) trending.observe(viewLifecycleOwner, ::showTrending)
feed.observe(viewLifecycleOwner, ::showFeed) feed.observe(viewLifecycleOwner, ::showFeed)
@ -123,11 +142,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
if (streamItems == null) return if (streamItems == null) return
makeVisible(binding.trendingRV, binding.trendingTV) makeVisible(binding.trendingRV, binding.trendingTV)
binding.trendingRV.layoutManager = GridLayoutManager(context, 2) trendingAdapter.submitList(streamItems)
binding.trendingRV.adapter = VideosAdapter(
streamItems.toMutableList(),
forceMode = LayoutMode.TRENDING_ROW
)
} }
private fun showFeed(streamItems: List<StreamItem>?) { private fun showFeed(streamItems: List<StreamItem>?) {
@ -138,57 +153,29 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
val feedVideos = streamItems val feedVideos = streamItems
.let { DatabaseHelper.filterByStatusAndWatchPosition(it, hideWatched) } .let { DatabaseHelper.filterByStatusAndWatchPosition(it, hideWatched) }
.take(20) .take(20)
.toMutableList()
with(binding.featuredRV) { feedAdapter.submitList(feedVideos)
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
adapter = VideosAdapter(feedVideos, forceMode = LayoutMode.RELATED_COLUMN)
}
} }
private fun showBookmarks(bookmarks: List<PlaylistBookmark>?) { private fun showBookmarks(bookmarks: List<PlaylistBookmark>?) {
if (bookmarks == null) return if (bookmarks == null) return
makeVisible(binding.bookmarksTV, binding.bookmarksRV) makeVisible(binding.bookmarksTV, binding.bookmarksRV)
with(binding.bookmarksRV) { bookmarkAdapter.submitList(bookmarks)
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
adapter = PlaylistBookmarkAdapter(
bookmarks.toMutableList(),
PlaylistBookmarkAdapter.Companion.BookmarkMode.HOME
)
}
} }
private fun showPlaylists(playlists: List<Playlists>?) { private fun showPlaylists(playlists: List<Playlists>?) {
if (playlists == null) return if (playlists == null) return
makeVisible(binding.playlistsRV, binding.playlistsTV) makeVisible(binding.playlistsRV, binding.playlistsTV)
binding.playlistsRV.layoutManager = LinearLayoutManager(context) playlistAdapter.submitList(playlists)
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
}
}
})
} }
private fun showContinueWatching(unwatchedVideos: List<StreamItem>?) { private fun showContinueWatching(unwatchedVideos: List<StreamItem>?) {
if (unwatchedVideos == null) return if (unwatchedVideos == null) return
makeVisible(binding.watchingRV, binding.watchingTV) makeVisible(binding.watchingRV, binding.watchingTV)
binding.watchingRV.layoutManager = LinearLayoutManager(context, HORIZONTAL, false) watchingAdapter.submitList(unwatchedVideos)
binding.watchingRV.adapter = VideosAdapter(
unwatchedVideos.toMutableList(),
forceMode = LayoutMode.RELATED_COLUMN
)
} }
private fun updateLoading(isLoading: Boolean) { 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 commonPlayerViewModel: CommonPlayerViewModel by activityViewModels()
private val playlistsAdapter = PlaylistsAdapter(PlaylistsHelper.getPrivatePlaylistType())
private val playlistBookmarkAdapter = PlaylistBookmarkAdapter()
override fun setLayoutManagers(gridItems: Int) { override fun setLayoutManagers(gridItems: Int) {
_binding?.bookmarksRecView?.layoutManager = GridLayoutManager(context, gridItems.ceilHalf()) _binding?.bookmarksRecView?.layoutManager = GridLayoutManager(context, gridItems.ceilHalf())
_binding?.playlistRecView?.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) _binding = FragmentLibraryBinding.bind(view)
super.onViewCreated(view, savedInstanceState) 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 // listen for the mini player state changing
commonPlayerViewModel.isMiniPlayerVisible.observe(viewLifecycleOwner) { commonPlayerViewModel.isMiniPlayerVisible.observe(viewLifecycleOwner) {
updateFABMargin(it) updateFABMargin(it)
@ -142,7 +157,7 @@ class LibraryFragment : DynamicLayoutManagerFragment(R.layout.fragment_library)
binding.bookmarksCV.isVisible = bookmarks.isNotEmpty() binding.bookmarksCV.isVisible = bookmarks.isNotEmpty()
if (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>) { private fun showPlaylists(playlists: List<Playlists>) {
val binding = _binding ?: return 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.nothingHere.isGone = true
binding.sortTV.isVisible = 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) { if (PlayerHelper.relatedStreamsEnabled) {
val relatedLayoutManager = binding.relatedRecView.layoutManager as LinearLayoutManager val relatedLayoutManager = binding.relatedRecView.layoutManager as LinearLayoutManager
binding.relatedRecView.adapter = VideosAdapter( binding.relatedRecView.adapter = VideosAdapter(
streams.relatedStreams.filter { !it.title.isNullOrBlank() }.toMutableList(),
forceMode = if (relatedLayoutManager.orientation == LinearLayoutManager.HORIZONTAL) { forceMode = if (relatedLayoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
VideosAdapter.Companion.LayoutMode.RELATED_COLUMN VideosAdapter.Companion.LayoutMode.RELATED_COLUMN
} else { } else {
VideosAdapter.Companion.LayoutMode.TRENDING_ROW VideosAdapter.Companion.LayoutMode.TRENDING_ROW
} }
) ).also { adapter ->
adapter.submitList(streams.relatedStreams.filter { !it.title.isNullOrBlank() })
}
} }
// update the subscribed state // update the subscribed state

View File

@ -325,6 +325,7 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
playlistId, playlistId,
playlistType playlistType
) )
// TODO make sure the adapter is set once in onViewCreated
binding.playlistRecView.adapter = playlistAdapter binding.playlistRecView.adapter = playlistAdapter
// listen for playlist items to become deleted // 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.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData 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 viewModel: SearchViewModel by activityViewModels()
private val mainActivity get() = activity as MainActivity 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel.searchQuery.value = arguments?.getString(IntentData.query) viewModel.searchQuery.value = arguments?.getString(IntentData.query)
@ -43,9 +65,15 @@ class SearchSuggestionsFragment : Fragment(R.layout.fragment_search_suggestions)
_binding = FragmentSearchSuggestionsBinding.bind(view) _binding = FragmentSearchSuggestionsBinding.bind(view)
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.suggestionsRecycler.layoutManager = LinearLayoutManager(requireContext()).apply { viewModel.searchQuery
reverseLayout = true .map { it.isNullOrEmpty() }
stackFromEnd = true .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 // waiting for the query to change
@ -81,30 +109,19 @@ class SearchSuggestionsFragment : Fragment(R.layout.fragment_search_suggestions)
return@launch return@launch
} }
// only load the suggestions if the input field didn't get cleared yet // only load the suggestions if the input field didn't get cleared yet
val suggestionsAdapter = SearchSuggestionsAdapter( if (!viewModel.searchQuery.value.isNullOrEmpty()) {
response.reversed(), suggestionsAdapter.submitList(response.reversed())
(activity as MainActivity).searchView
)
if (isAdded && !viewModel.searchQuery.value.isNullOrEmpty()) {
binding.suggestionsRecycler.adapter = suggestionsAdapter
} }
} }
} }
private fun showHistory() { private fun showHistory() {
val searchView = runCatching {
(activity as MainActivity).searchView
}.getOrNull()
lifecycleScope.launch { lifecycleScope.launch {
val historyList = withContext(Dispatchers.IO) { val historyList = withContext(Dispatchers.IO) {
Database.searchHistoryDao().getAll().map { it.query } Database.searchHistoryDao().getAll().map { it.query }
} }
if (historyList.isNotEmpty() && searchView != null) { if (historyList.isNotEmpty()) {
binding.suggestionsRecycler.adapter = SearchHistoryAdapter( historyAdapter.submitList(historyList)
historyList,
searchView
)
} else { } else {
binding.suggestionsRecycler.isGone = true binding.suggestionsRecycler.isGone = true
binding.historyEmpty.isVisible = true binding.historyEmpty.isVisible = true

View File

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

View File

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

View File

@ -50,6 +50,8 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
private var isLoading = false private var isLoading = false
private var recyclerViewState: Parcelable? = null private var recyclerViewState: Parcelable? = null
private val watchHistoryAdapter = WatchHistoryAdapter()
private var selectedStatusFilter = PreferenceHelper.getInt( private var selectedStatusFilter = PreferenceHelper.getInt(
PreferenceKeys.SELECTED_HISTORY_STATUS_FILTER, PreferenceKeys.SELECTED_HISTORY_STATUS_FILTER,
0 0
@ -80,6 +82,31 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
_binding?.watchHistoryRecView?.updatePadding(bottom = if (it) 64f.dpToPx() else 0) _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 { lifecycleScope.launch {
val history = withContext(Dispatchers.IO) { val history = withContext(Dispatchers.IO) {
DatabaseHelper.getWatchHistoryPage(1, HISTORY_PAGE_SIZE) DatabaseHelper.getWatchHistoryPage(1, HISTORY_PAGE_SIZE)
@ -139,14 +166,6 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
}.show(childFragmentManager) }.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) showWatchHistory(history)
} }
@ -157,7 +176,6 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
private fun showWatchHistory(history: List<WatchHistoryItem>) { private fun showWatchHistory(history: List<WatchHistoryItem>) {
val watchHistory = history.filterByStatusAndWatchPosition() val watchHistory = history.filterByStatusAndWatchPosition()
val watchHistoryAdapter = WatchHistoryAdapter(watchHistory.toMutableList())
binding.playAll.setOnClickListener { binding.playAll.setOnClickListener {
PlayingQueue.resetToDefaults() PlayingQueue.resetToDefaults()
@ -170,26 +188,10 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
keepQueue = true keepQueue = true
) )
} }
watchHistoryAdapter.submitList(history)
binding.watchHistoryRecView.adapter = watchHistoryAdapter
binding.historyEmpty.isGone = true binding.historyEmpty.isGone = true
binding.historyContainer.isVisible = 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 // add a listener for scroll end, delay needed to prevent loading new ones the first time
handler.postDelayed(200) { handler.postDelayed(200) {
if (_binding == null) return@postDelayed if (_binding == null) return@postDelayed

View File

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

View File

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

View File

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

View File

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

View File

@ -20,8 +20,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checkable="false" android:checkable="false"
android:visibility="gone" android:text="Sponsor"
android:text="Sponsor"/> android:visibility="gone" />
</LinearLayout> </LinearLayout>
@ -133,7 +133,9 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/tags_recycler" android:id="@+id/tags_recycler"
android:layout_width="wrap_content" 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> </LinearLayout>

View File

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

View File

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

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -9,7 +10,10 @@
android:id="@+id/suggestions_recycler" android:id="@+id/suggestions_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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 <LinearLayout
android:id="@+id/history_empty" android:id="@+id/history_empty"