mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 16:00:31 +05:30
refactor: Rewrite search functionality using Paging (#5528)
* refactor: Rewrite search functionality using Paging * Apply code review comments * Update app/src/main/java/com/github/libretube/ui/adapters/SearchChannelAdapter.kt Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com> * Apply code review suggestions --------- Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com>
This commit is contained in:
parent
a9cfd48ffd
commit
858dbe7ede
@ -141,4 +141,7 @@ dependencies {
|
|||||||
/* Baseline profile generation */
|
/* Baseline profile generation */
|
||||||
implementation(libs.androidx.profileinstaller)
|
implementation(libs.androidx.profileinstaller)
|
||||||
"baselineProfile"(project(":baselineprofile"))
|
"baselineProfile"(project(":baselineprofile"))
|
||||||
|
|
||||||
|
/* AndroidX Paging */
|
||||||
|
implementation(libs.androidx.paging)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,185 @@
|
|||||||
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.api.JsonHelper
|
||||||
|
import com.github.libretube.api.obj.ContentItem
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.constants.IntentData
|
||||||
|
import com.github.libretube.databinding.ChannelRowBinding
|
||||||
|
import com.github.libretube.databinding.PlaylistsRowBinding
|
||||||
|
import com.github.libretube.databinding.VideoRowBinding
|
||||||
|
import com.github.libretube.enums.PlaylistType
|
||||||
|
import com.github.libretube.extensions.formatShort
|
||||||
|
import com.github.libretube.extensions.toID
|
||||||
|
import com.github.libretube.helpers.ImageHelper
|
||||||
|
import com.github.libretube.helpers.NavigationHelper
|
||||||
|
import com.github.libretube.ui.adapters.callbacks.SearchCallback
|
||||||
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
|
import com.github.libretube.ui.extensions.setFormattedDuration
|
||||||
|
import com.github.libretube.ui.extensions.setWatchProgressLength
|
||||||
|
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||||
|
import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
|
||||||
|
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
|
||||||
|
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
|
||||||
|
import com.github.libretube.ui.viewholders.SearchViewHolder
|
||||||
|
import com.github.libretube.util.TextUtils
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
|
// TODO: Replace with SearchResultsAdapter when migrating the channel fragment to use Paging as well
|
||||||
|
class SearchChannelAdapter : ListAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
|
||||||
|
val layoutInflater = LayoutInflater.from(parent.context)
|
||||||
|
|
||||||
|
return when (viewType) {
|
||||||
|
0 -> SearchViewHolder(VideoRowBinding.inflate(layoutInflater, parent, false))
|
||||||
|
1 -> SearchViewHolder(ChannelRowBinding.inflate(layoutInflater, parent, false))
|
||||||
|
2 -> SearchViewHolder(PlaylistsRowBinding.inflate(layoutInflater, parent, false))
|
||||||
|
else -> throw IllegalArgumentException("Invalid type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
|
||||||
|
val searchItem = currentList[position]
|
||||||
|
|
||||||
|
val videoRowBinding = holder.videoRowBinding
|
||||||
|
val channelRowBinding = holder.channelRowBinding
|
||||||
|
val playlistRowBinding = holder.playlistRowBinding
|
||||||
|
|
||||||
|
if (videoRowBinding != null) {
|
||||||
|
bindVideo(searchItem, videoRowBinding, position)
|
||||||
|
} else if (channelRowBinding != null) {
|
||||||
|
bindChannel(searchItem, channelRowBinding)
|
||||||
|
} else if (playlistRowBinding != null) {
|
||||||
|
bindPlaylist(searchItem, playlistRowBinding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (currentList[position].type) {
|
||||||
|
StreamItem.TYPE_STREAM -> 0
|
||||||
|
StreamItem.TYPE_CHANNEL -> 1
|
||||||
|
StreamItem.TYPE_PLAYLIST -> 2
|
||||||
|
else -> 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindVideo(item: ContentItem, binding: VideoRowBinding, position: Int) {
|
||||||
|
binding.apply {
|
||||||
|
thumbnail.setImageDrawable(null)
|
||||||
|
ImageHelper.loadImage(item.thumbnail, thumbnail)
|
||||||
|
thumbnailDuration.setFormattedDuration(item.duration, item.isShort)
|
||||||
|
videoTitle.text = item.title
|
||||||
|
|
||||||
|
val viewsString = item.views.takeIf { it != -1L }?.formatShort().orEmpty()
|
||||||
|
val uploadDate = item.uploaded.takeIf { it > 0 }?.let {
|
||||||
|
" ${TextUtils.SEPARATOR} ${TextUtils.formatRelativeDate(root.context, it)}"
|
||||||
|
}.orEmpty()
|
||||||
|
videoInfo.text = root.context.getString(
|
||||||
|
R.string.normal_views,
|
||||||
|
viewsString,
|
||||||
|
uploadDate
|
||||||
|
)
|
||||||
|
|
||||||
|
channelContainer.isGone = true
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
NavigationHelper.navigateVideo(root.context, item.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
val videoId = item.url.toID()
|
||||||
|
val activity = (root.context as BaseActivity)
|
||||||
|
val fragmentManager = activity.supportFragmentManager
|
||||||
|
root.setOnLongClickListener {
|
||||||
|
fragmentManager.setFragmentResultListener(
|
||||||
|
VideoOptionsBottomSheet.VIDEO_OPTIONS_SHEET_REQUEST_KEY,
|
||||||
|
activity
|
||||||
|
) { _, _ ->
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
val sheet = VideoOptionsBottomSheet()
|
||||||
|
val contentItemString = JsonHelper.json.encodeToString(item)
|
||||||
|
val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString)
|
||||||
|
sheet.arguments = bundleOf(IntentData.streamItem to streamItem)
|
||||||
|
sheet.show(fragmentManager, SearchChannelAdapter::class.java.name)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
channelContainer.setOnClickListener {
|
||||||
|
NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
|
||||||
|
}
|
||||||
|
watchProgress.setWatchProgressLength(videoId, item.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindChannel(item: ContentItem, binding: ChannelRowBinding) {
|
||||||
|
binding.apply {
|
||||||
|
searchChannelImage.setImageDrawable(null)
|
||||||
|
ImageHelper.loadImage(item.thumbnail, searchChannelImage, true)
|
||||||
|
searchChannelName.text = item.name
|
||||||
|
|
||||||
|
val subscribers = item.subscribers.formatShort()
|
||||||
|
searchViews.text = if (item.subscribers >= 0 && item.videos >= 0) {
|
||||||
|
root.context.getString(R.string.subscriberAndVideoCounts, subscribers, item.videos)
|
||||||
|
} else if (item.subscribers >= 0) {
|
||||||
|
root.context.getString(R.string.subscribers, subscribers)
|
||||||
|
} else if (item.videos >= 0) {
|
||||||
|
root.context.getString(R.string.videoCount, item.videos)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
root.setOnClickListener {
|
||||||
|
NavigationHelper.navigateChannel(root.context, item.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscribed = false
|
||||||
|
binding.searchSubButton.setupSubscriptionButton(item.url.toID(), item.name?.toID()) {
|
||||||
|
subscribed = it
|
||||||
|
}
|
||||||
|
|
||||||
|
root.setOnLongClickListener {
|
||||||
|
val channelOptionsSheet = ChannelOptionsBottomSheet()
|
||||||
|
channelOptionsSheet.arguments = bundleOf(
|
||||||
|
IntentData.channelId to item.url.toID(),
|
||||||
|
IntentData.channelName to item.name,
|
||||||
|
IntentData.isSubscribed to subscribed
|
||||||
|
)
|
||||||
|
channelOptionsSheet.show((root.context as BaseActivity).supportFragmentManager)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindPlaylist(item: ContentItem, binding: PlaylistsRowBinding) {
|
||||||
|
binding.apply {
|
||||||
|
playlistThumbnail.setImageDrawable(null)
|
||||||
|
ImageHelper.loadImage(item.thumbnail, playlistThumbnail)
|
||||||
|
if (item.videos != -1L) videoCount.text = item.videos.toString()
|
||||||
|
playlistTitle.text = item.name
|
||||||
|
playlistDescription.text = item.uploaderName
|
||||||
|
root.setOnClickListener {
|
||||||
|
NavigationHelper.navigatePlaylist(root.context, item.url, PlaylistType.PUBLIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.setOnLongClickListener {
|
||||||
|
val playlistId = item.url.toID()
|
||||||
|
val playlistName = item.name!!
|
||||||
|
val sheet = PlaylistOptionsBottomSheet()
|
||||||
|
sheet.arguments = bundleOf(
|
||||||
|
IntentData.playlistId to playlistId,
|
||||||
|
IntentData.playlistName to playlistName,
|
||||||
|
IntentData.playlistType to PlaylistType.PUBLIC
|
||||||
|
)
|
||||||
|
sheet.show(
|
||||||
|
(root.context as BaseActivity).supportFragmentManager,
|
||||||
|
PlaylistOptionsBottomSheet::class.java.name
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,7 @@ 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.core.view.isGone
|
import androidx.paging.PagingDataAdapter
|
||||||
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.JsonHelper
|
import com.github.libretube.api.JsonHelper
|
||||||
import com.github.libretube.api.obj.ContentItem
|
import com.github.libretube.api.obj.ContentItem
|
||||||
@ -19,6 +17,7 @@ import com.github.libretube.extensions.formatShort
|
|||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
import com.github.libretube.helpers.ImageHelper
|
import com.github.libretube.helpers.ImageHelper
|
||||||
import com.github.libretube.helpers.NavigationHelper
|
import com.github.libretube.helpers.NavigationHelper
|
||||||
|
import com.github.libretube.ui.adapters.callbacks.SearchCallback
|
||||||
import com.github.libretube.ui.base.BaseActivity
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
import com.github.libretube.ui.extensions.setFormattedDuration
|
import com.github.libretube.ui.extensions.setFormattedDuration
|
||||||
import com.github.libretube.ui.extensions.setWatchProgressLength
|
import com.github.libretube.ui.extensions.setWatchProgressLength
|
||||||
@ -30,10 +29,9 @@ import com.github.libretube.ui.viewholders.SearchViewHolder
|
|||||||
import com.github.libretube.util.TextUtils
|
import com.github.libretube.util.TextUtils
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
class SearchAdapter(
|
class SearchResultsAdapter(
|
||||||
private val isChannelAdapter: Boolean = false,
|
|
||||||
private val timeStamp: Long = 0
|
private val timeStamp: Long = 0
|
||||||
) : ListAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
|
) : PagingDataAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
|
||||||
val layoutInflater = LayoutInflater.from(parent.context)
|
val layoutInflater = LayoutInflater.from(parent.context)
|
||||||
|
|
||||||
@ -55,7 +53,7 @@ class SearchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
|
||||||
val searchItem = currentList[position]
|
val searchItem = getItem(position)!!
|
||||||
|
|
||||||
val videoRowBinding = holder.videoRowBinding
|
val videoRowBinding = holder.videoRowBinding
|
||||||
val channelRowBinding = holder.channelRowBinding
|
val channelRowBinding = holder.channelRowBinding
|
||||||
@ -71,7 +69,7 @@ class SearchAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return when (currentList[position].type) {
|
return when (getItem(position)?.type) {
|
||||||
StreamItem.TYPE_STREAM -> 0
|
StreamItem.TYPE_STREAM -> 0
|
||||||
StreamItem.TYPE_CHANNEL -> 1
|
StreamItem.TYPE_CHANNEL -> 1
|
||||||
StreamItem.TYPE_PLAYLIST -> 2
|
StreamItem.TYPE_PLAYLIST -> 2
|
||||||
@ -95,13 +93,8 @@ class SearchAdapter(
|
|||||||
uploadDate
|
uploadDate
|
||||||
)
|
)
|
||||||
|
|
||||||
// only display channel related info if not in a channel tab
|
channelName.text = item.uploaderName
|
||||||
if (!isChannelAdapter) {
|
ImageHelper.loadImage(item.uploaderAvatar, channelImage, true)
|
||||||
channelName.text = item.uploaderName
|
|
||||||
ImageHelper.loadImage(item.uploaderAvatar, channelImage, true)
|
|
||||||
} else {
|
|
||||||
channelContainer.isGone = true
|
|
||||||
}
|
|
||||||
|
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
NavigationHelper.navigateVideo(root.context, item.url, timestamp = timeStamp)
|
NavigationHelper.navigateVideo(root.context, item.url, timestamp = timeStamp)
|
||||||
@ -121,7 +114,7 @@ class SearchAdapter(
|
|||||||
val contentItemString = JsonHelper.json.encodeToString(item)
|
val contentItemString = JsonHelper.json.encodeToString(item)
|
||||||
val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString)
|
val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString)
|
||||||
sheet.arguments = bundleOf(IntentData.streamItem to streamItem)
|
sheet.arguments = bundleOf(IntentData.streamItem to streamItem)
|
||||||
sheet.show(fragmentManager, SearchAdapter::class.java.name)
|
sheet.show(fragmentManager, SearchResultsAdapter::class.java.name)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
channelContainer.setOnClickListener {
|
channelContainer.setOnClickListener {
|
||||||
@ -196,14 +189,4 @@ class SearchAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object SearchCallback : DiffUtil.ItemCallback<ContentItem>() {
|
|
||||||
override fun areItemsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
|
|
||||||
return oldItem.url == newItem.url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.github.libretube.ui.adapters.callbacks
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import com.github.libretube.api.obj.ContentItem
|
||||||
|
|
||||||
|
object SearchCallback : DiffUtil.ItemCallback<ContentItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
|
||||||
|
return oldItem.url == newItem.url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ContentItem, newItem: ContentItem) = true
|
||||||
|
}
|
@ -26,7 +26,7 @@ import com.github.libretube.helpers.ImageHelper
|
|||||||
import com.github.libretube.helpers.NavigationHelper
|
import com.github.libretube.helpers.NavigationHelper
|
||||||
import com.github.libretube.obj.ChannelTabs
|
import com.github.libretube.obj.ChannelTabs
|
||||||
import com.github.libretube.obj.ShareData
|
import com.github.libretube.obj.ShareData
|
||||||
import com.github.libretube.ui.adapters.SearchAdapter
|
import com.github.libretube.ui.adapters.SearchChannelAdapter
|
||||||
import com.github.libretube.ui.adapters.VideosAdapter
|
import com.github.libretube.ui.adapters.VideosAdapter
|
||||||
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
@ -57,7 +57,7 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
|||||||
)
|
)
|
||||||
private var channelTabs: List<ChannelTab> = emptyList()
|
private var channelTabs: List<ChannelTab> = emptyList()
|
||||||
private var nextPages = Array<String?>(5) { null }
|
private var nextPages = Array<String?>(5) { null }
|
||||||
private var searchAdapter: SearchAdapter? = null
|
private var searchChannelAdapter: SearchChannelAdapter? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -293,9 +293,9 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
|||||||
|
|
||||||
val binding = _binding ?: return@launch
|
val binding = _binding ?: return@launch
|
||||||
|
|
||||||
searchAdapter = SearchAdapter(true)
|
searchChannelAdapter = SearchChannelAdapter()
|
||||||
binding.channelRecView.adapter = searchAdapter
|
binding.channelRecView.adapter = searchChannelAdapter
|
||||||
searchAdapter?.submitList(response.content)
|
searchChannelAdapter?.submitList(response.content)
|
||||||
|
|
||||||
binding.channelRefresh.isRefreshing = false
|
binding.channelRefresh.isRefreshing = false
|
||||||
isLoading = false
|
isLoading = false
|
||||||
@ -320,7 +320,7 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
|||||||
content = content.deArrow()
|
content = content.deArrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
searchAdapter?.let {
|
searchChannelAdapter?.let {
|
||||||
it.submitList(it.currentList + newContent.content)
|
it.submitList(it.currentList + newContent.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,47 +1,40 @@
|
|||||||
package com.github.libretube.ui.fragments
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.SoftwareKeyboardControllerCompat
|
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.databinding.FragmentSearchResultBinding
|
import com.github.libretube.databinding.FragmentSearchResultBinding
|
||||||
import com.github.libretube.db.DatabaseHelper
|
import com.github.libretube.db.DatabaseHelper
|
||||||
import com.github.libretube.db.obj.SearchHistoryItem
|
import com.github.libretube.db.obj.SearchHistoryItem
|
||||||
import com.github.libretube.extensions.TAG
|
|
||||||
import com.github.libretube.extensions.ceilHalf
|
import com.github.libretube.extensions.ceilHalf
|
||||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
import com.github.libretube.ui.activities.MainActivity
|
import com.github.libretube.ui.activities.MainActivity
|
||||||
import com.github.libretube.ui.adapters.SearchAdapter
|
import com.github.libretube.ui.adapters.SearchResultsAdapter
|
||||||
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.models.SearchResultViewModel
|
||||||
import com.github.libretube.util.TextUtils
|
|
||||||
import com.github.libretube.util.TextUtils.toTimeInSeconds
|
import com.github.libretube.util.TextUtils.toTimeInSeconds
|
||||||
import com.github.libretube.util.deArrow
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
class SearchResultFragment : DynamicLayoutManagerFragment() {
|
class SearchResultFragment : DynamicLayoutManagerFragment() {
|
||||||
private var _binding: FragmentSearchResultBinding? = null
|
private var _binding: FragmentSearchResultBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private val args by navArgs<SearchResultFragmentArgs>()
|
private val args by navArgs<SearchResultFragmentArgs>()
|
||||||
|
private val viewModel by viewModels<SearchResultViewModel>()
|
||||||
private var nextPage: String? = null
|
|
||||||
|
|
||||||
private lateinit var searchAdapter: SearchAdapter
|
|
||||||
private var searchFilter = "all"
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -68,7 +61,7 @@ class SearchResultFragment : DynamicLayoutManagerFragment() {
|
|||||||
|
|
||||||
// filter options
|
// filter options
|
||||||
binding.filterChipGroup.setOnCheckedStateChangeListener { _, _ ->
|
binding.filterChipGroup.setOnCheckedStateChangeListener { _, _ ->
|
||||||
searchFilter = when (
|
viewModel.setFilter(when (
|
||||||
binding.filterChipGroup.checkedChipId
|
binding.filterChipGroup.checkedChipId
|
||||||
) {
|
) {
|
||||||
R.id.chip_all -> "all"
|
R.id.chip_all -> "all"
|
||||||
@ -81,80 +74,28 @@ class SearchResultFragment : DynamicLayoutManagerFragment() {
|
|||||||
R.id.chip_music_playlists -> "music_playlists"
|
R.id.chip_music_playlists -> "music_playlists"
|
||||||
R.id.chip_music_artists -> "music_artists"
|
R.id.chip_music_artists -> "music_artists"
|
||||||
else -> throw IllegalArgumentException("Filter out of range")
|
else -> throw IllegalArgumentException("Filter out of range")
|
||||||
}
|
})
|
||||||
fetchSearch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchSearch()
|
val timeStamp = args.query.toHttpUrlOrNull()?.queryParameter("t")?.toTimeInSeconds()
|
||||||
|
val searchResultsAdapter = SearchResultsAdapter(timeStamp ?: 0)
|
||||||
|
binding.searchRecycler.adapter = searchResultsAdapter
|
||||||
|
|
||||||
binding.searchRecycler.viewTreeObserver.addOnScrollChangedListener {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
if (_binding?.searchRecycler?.canScrollVertically(1) == false &&
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
nextPage != null
|
searchResultsAdapter.loadStateFlow.collect {
|
||||||
) {
|
val isLoading = it.source.refresh is LoadState.Loading
|
||||||
fetchNextSearchItems()
|
binding.progress.isVisible = isLoading
|
||||||
}
|
binding.searchResultsLayout.isGone = isLoading
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchSearch() {
|
|
||||||
_binding?.progress?.isVisible = true
|
|
||||||
_binding?.searchResultsLayout?.isGone = true
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
var timeStamp: Long? = null
|
|
||||||
|
|
||||||
// parse search URLs from YouTube entered in the search bar
|
|
||||||
val searchQuery = args.query.toHttpUrlOrNull()?.let {
|
|
||||||
val videoId = TextUtils.getVideoIdFromUrl(it.toString()) ?: args.query
|
|
||||||
timeStamp = it.queryParameter("t")?.toTimeInSeconds()
|
|
||||||
"${ShareDialog.YOUTUBE_FRONTEND_URL}/watch?v=$videoId"
|
|
||||||
} ?: args.query
|
|
||||||
|
|
||||||
view?.let { SoftwareKeyboardControllerCompat(it).hide() }
|
|
||||||
val response = try {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getSearchResults(searchQuery, searchFilter).apply {
|
|
||||||
items = items.deArrow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG(), e.toString())
|
|
||||||
context?.toastFromMainDispatcher(R.string.unknown_error)
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val binding = _binding ?: return@launch
|
|
||||||
searchAdapter = SearchAdapter(timeStamp = timeStamp ?: 0)
|
|
||||||
binding.searchRecycler.adapter = searchAdapter
|
|
||||||
searchAdapter.submitList(response.items)
|
|
||||||
|
|
||||||
binding.searchResultsLayout.isVisible = true
|
|
||||||
binding.progress.isGone = true
|
|
||||||
binding.noSearchResult.isVisible = response.items.isEmpty()
|
|
||||||
|
|
||||||
nextPage = response.nextpage
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchNextSearchItems() {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
lifecycleScope.launch {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
val response = try {
|
viewModel.searchResultsFlow.collectLatest {
|
||||||
withContext(Dispatchers.IO) {
|
searchResultsAdapter.submitData(it)
|
||||||
RetrofitInstance.api.getSearchResultsNextPage(
|
|
||||||
args.query,
|
|
||||||
searchFilter,
|
|
||||||
nextPage!!
|
|
||||||
).apply {
|
|
||||||
items = items.deArrow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG(), e.toString())
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
nextPage = response.nextpage
|
|
||||||
if (response.items.isNotEmpty()) {
|
|
||||||
searchAdapter.submitList(searchAdapter.currentList + response.items)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
package com.github.libretube.ui.models
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.cachedIn
|
||||||
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
|
import com.github.libretube.ui.fragments.SearchResultFragmentArgs
|
||||||
|
import com.github.libretube.ui.models.sources.SearchPagingSource
|
||||||
|
import com.github.libretube.util.TextUtils
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
|
class SearchResultViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
|
||||||
|
private val args = SearchResultFragmentArgs.fromSavedStateHandle(savedStateHandle)
|
||||||
|
// parse search URLs from YouTube entered in the search bar
|
||||||
|
private val searchQuery = args.query.toHttpUrlOrNull()?.let {
|
||||||
|
val videoId = TextUtils.getVideoIdFromUrl(it.toString()) ?: args.query
|
||||||
|
"${ShareDialog.YOUTUBE_FRONTEND_URL}/watch?v=$videoId"
|
||||||
|
} ?: args.query
|
||||||
|
|
||||||
|
private val filterMutableData = MutableStateFlow("all")
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val searchResultsFlow = filterMutableData.flatMapLatest {
|
||||||
|
Pager(
|
||||||
|
PagingConfig(pageSize = 20, enablePlaceholders = false),
|
||||||
|
pagingSourceFactory = { SearchPagingSource(searchQuery, it) }
|
||||||
|
).flow
|
||||||
|
}
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
fun setFilter(filter: String) {
|
||||||
|
filterMutableData.value = filter
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.github.libretube.ui.models.sources
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.github.libretube.api.RetrofitInstance
|
||||||
|
import com.github.libretube.api.obj.ContentItem
|
||||||
|
import com.github.libretube.util.deArrow
|
||||||
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
class SearchPagingSource(
|
||||||
|
private val searchQuery: String,
|
||||||
|
private val searchFilter: String
|
||||||
|
): PagingSource<String, ContentItem>() {
|
||||||
|
override fun getRefreshKey(state: PagingState<String, ContentItem>) = null
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, ContentItem> {
|
||||||
|
return try {
|
||||||
|
val result = params.key?.let {
|
||||||
|
RetrofitInstance.api.getSearchResultsNextPage(searchQuery, searchFilter, it)
|
||||||
|
} ?: RetrofitInstance.api.getSearchResults(searchQuery, searchFilter)
|
||||||
|
LoadResult.Page(result.items.deArrow(), null, result.nextpage)
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@ uiautomator = "2.2.0"
|
|||||||
benchmarkMacroJunit4 = "1.2.3"
|
benchmarkMacroJunit4 = "1.2.3"
|
||||||
baselineprofile = "1.2.3"
|
baselineprofile = "1.2.3"
|
||||||
profileinstaller = "1.3.1"
|
profileinstaller = "1.3.1"
|
||||||
|
paging = "3.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
|
androidx-activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
|
||||||
@ -72,6 +73,7 @@ kotlinx-serialization-retrofit = { group = "com.jakewharton.retrofit", name = "r
|
|||||||
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
||||||
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
|
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
|
||||||
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
||||||
|
androidx-paging = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "paging" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidTest = { id = "com.android.test", version.ref = "gradle" }
|
androidTest = { id = "com.android.test", version.ref = "gradle" }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user