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 */
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
"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.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.JsonHelper
|
||||
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.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
|
||||
@ -30,10 +29,9 @@ import com.github.libretube.ui.viewholders.SearchViewHolder
|
||||
import com.github.libretube.util.TextUtils
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
class SearchAdapter(
|
||||
private val isChannelAdapter: Boolean = false,
|
||||
class SearchResultsAdapter(
|
||||
private val timeStamp: Long = 0
|
||||
) : ListAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
|
||||
) : PagingDataAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
|
||||
@ -55,7 +53,7 @@ class SearchAdapter(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
|
||||
val searchItem = currentList[position]
|
||||
val searchItem = getItem(position)!!
|
||||
|
||||
val videoRowBinding = holder.videoRowBinding
|
||||
val channelRowBinding = holder.channelRowBinding
|
||||
@ -71,7 +69,7 @@ class SearchAdapter(
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (currentList[position].type) {
|
||||
return when (getItem(position)?.type) {
|
||||
StreamItem.TYPE_STREAM -> 0
|
||||
StreamItem.TYPE_CHANNEL -> 1
|
||||
StreamItem.TYPE_PLAYLIST -> 2
|
||||
@ -95,13 +93,8 @@ class SearchAdapter(
|
||||
uploadDate
|
||||
)
|
||||
|
||||
// only display channel related info if not in a channel tab
|
||||
if (!isChannelAdapter) {
|
||||
channelName.text = item.uploaderName
|
||||
ImageHelper.loadImage(item.uploaderAvatar, channelImage, true)
|
||||
} else {
|
||||
channelContainer.isGone = true
|
||||
}
|
||||
|
||||
root.setOnClickListener {
|
||||
NavigationHelper.navigateVideo(root.context, item.url, timestamp = timeStamp)
|
||||
@ -121,7 +114,7 @@ class SearchAdapter(
|
||||
val contentItemString = JsonHelper.json.encodeToString(item)
|
||||
val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString)
|
||||
sheet.arguments = bundleOf(IntentData.streamItem to streamItem)
|
||||
sheet.show(fragmentManager, SearchAdapter::class.java.name)
|
||||
sheet.show(fragmentManager, SearchResultsAdapter::class.java.name)
|
||||
true
|
||||
}
|
||||
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.obj.ChannelTabs
|
||||
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.base.DynamicLayoutManagerFragment
|
||||
import com.github.libretube.ui.dialogs.ShareDialog
|
||||
@ -57,7 +57,7 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
||||
)
|
||||
private var channelTabs: List<ChannelTab> = emptyList()
|
||||
private var nextPages = Array<String?>(5) { null }
|
||||
private var searchAdapter: SearchAdapter? = null
|
||||
private var searchChannelAdapter: SearchChannelAdapter? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -293,9 +293,9 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
||||
|
||||
val binding = _binding ?: return@launch
|
||||
|
||||
searchAdapter = SearchAdapter(true)
|
||||
binding.channelRecView.adapter = searchAdapter
|
||||
searchAdapter?.submitList(response.content)
|
||||
searchChannelAdapter = SearchChannelAdapter()
|
||||
binding.channelRecView.adapter = searchChannelAdapter
|
||||
searchChannelAdapter?.submitList(response.content)
|
||||
|
||||
binding.channelRefresh.isRefreshing = false
|
||||
isLoading = false
|
||||
@ -320,7 +320,7 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
||||
content = content.deArrow()
|
||||
}
|
||||
|
||||
searchAdapter?.let {
|
||||
searchChannelAdapter?.let {
|
||||
it.submitList(it.currentList + newContent.content)
|
||||
}
|
||||
|
||||
|
@ -1,47 +1,40 @@
|
||||
package com.github.libretube.ui.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.SoftwareKeyboardControllerCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.databinding.FragmentSearchResultBinding
|
||||
import com.github.libretube.db.DatabaseHelper
|
||||
import com.github.libretube.db.obj.SearchHistoryItem
|
||||
import com.github.libretube.extensions.TAG
|
||||
import com.github.libretube.extensions.ceilHalf
|
||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||
import com.github.libretube.helpers.PreferenceHelper
|
||||
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.dialogs.ShareDialog
|
||||
import com.github.libretube.util.TextUtils
|
||||
import com.github.libretube.ui.models.SearchResultViewModel
|
||||
import com.github.libretube.util.TextUtils.toTimeInSeconds
|
||||
import com.github.libretube.util.deArrow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
class SearchResultFragment : DynamicLayoutManagerFragment() {
|
||||
private var _binding: FragmentSearchResultBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val args by navArgs<SearchResultFragmentArgs>()
|
||||
|
||||
private var nextPage: String? = null
|
||||
|
||||
private lateinit var searchAdapter: SearchAdapter
|
||||
private var searchFilter = "all"
|
||||
private val viewModel by viewModels<SearchResultViewModel>()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@ -68,7 +61,7 @@ class SearchResultFragment : DynamicLayoutManagerFragment() {
|
||||
|
||||
// filter options
|
||||
binding.filterChipGroup.setOnCheckedStateChangeListener { _, _ ->
|
||||
searchFilter = when (
|
||||
viewModel.setFilter(when (
|
||||
binding.filterChipGroup.checkedChipId
|
||||
) {
|
||||
R.id.chip_all -> "all"
|
||||
@ -81,81 +74,29 @@ class SearchResultFragment : DynamicLayoutManagerFragment() {
|
||||
R.id.chip_music_playlists -> "music_playlists"
|
||||
R.id.chip_music_artists -> "music_artists"
|
||||
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 {
|
||||
if (_binding?.searchRecycler?.canScrollVertically(1) == false &&
|
||||
nextPage != null
|
||||
) {
|
||||
fetchNextSearchItems()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
searchResultsAdapter.loadStateFlow.collect {
|
||||
val isLoading = it.source.refresh is LoadState.Loading
|
||||
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()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.searchResultsFlow.collectLatest {
|
||||
searchResultsAdapter.submitData(it)
|
||||
}
|
||||
}
|
||||
} 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() {
|
||||
lifecycleScope.launch {
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
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"
|
||||
baselineprofile = "1.2.3"
|
||||
profileinstaller = "1.3.1"
|
||||
paging = "3.2.1"
|
||||
|
||||
[libraries]
|
||||
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-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
|
||||
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
||||
androidx-paging = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "paging" }
|
||||
|
||||
[plugins]
|
||||
androidTest = { id = "com.android.test", version.ref = "gradle" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user