From 858dbe7ede83f7fb8dbb7c191918f1df262e6a6c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne <31027858+Isira-Seneviratne@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:30:40 +0530 Subject: [PATCH] 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> --- app/build.gradle.kts | 3 + .../ui/adapters/SearchChannelAdapter.kt | 185 ++++++++++++++++++ ...archAdapter.kt => SearchResultsAdapter.kt} | 35 +--- .../ui/adapters/callbacks/SearchCallback.kt | 12 ++ .../libretube/ui/fragments/ChannelFragment.kt | 12 +- .../ui/fragments/SearchResultFragment.kt | 105 +++------- .../ui/models/SearchResultViewModel.kt | 39 ++++ .../ui/models/sources/SearchPagingSource.kt | 26 +++ gradle/libs.versions.toml | 2 + 9 files changed, 305 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/ui/adapters/SearchChannelAdapter.kt rename app/src/main/java/com/github/libretube/ui/adapters/{SearchAdapter.kt => SearchResultsAdapter.kt} (87%) create mode 100644 app/src/main/java/com/github/libretube/ui/adapters/callbacks/SearchCallback.kt create mode 100644 app/src/main/java/com/github/libretube/ui/models/SearchResultViewModel.kt create mode 100644 app/src/main/java/com/github/libretube/ui/models/sources/SearchPagingSource.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 11f15f795..071352e67 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,4 +141,7 @@ dependencies { /* Baseline profile generation */ implementation(libs.androidx.profileinstaller) "baselineProfile"(project(":baselineprofile")) + + /* AndroidX Paging */ + implementation(libs.androidx.paging) } diff --git a/app/src/main/java/com/github/libretube/ui/adapters/SearchChannelAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/SearchChannelAdapter.kt new file mode 100644 index 000000000..0f2fc74fd --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/adapters/SearchChannelAdapter.kt @@ -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(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 + } + } + } +} diff --git a/app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/SearchResultsAdapter.kt similarity index 87% rename from app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt rename to app/src/main/java/com/github/libretube/ui/adapters/SearchResultsAdapter.kt index a765ee77e..39a224876 100644 --- a/app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt +++ b/app/src/main/java/com/github/libretube/ui/adapters/SearchResultsAdapter.kt @@ -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(SearchCallback) { +) : PagingDataAdapter(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 - } + channelName.text = item.uploaderName + ImageHelper.loadImage(item.uploaderAvatar, channelImage, 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() { - override fun areItemsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean { - return oldItem.url == newItem.url - } - - override fun areContentsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean { - return true - } - } } diff --git a/app/src/main/java/com/github/libretube/ui/adapters/callbacks/SearchCallback.kt b/app/src/main/java/com/github/libretube/ui/adapters/callbacks/SearchCallback.kt new file mode 100644 index 000000000..3c0653d29 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/adapters/callbacks/SearchCallback.kt @@ -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() { + override fun areItemsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean { + return oldItem.url == newItem.url + } + + override fun areContentsTheSame(oldItem: ContentItem, newItem: ContentItem) = true +} diff --git a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt index b4826249c..0a6d27b7c 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt @@ -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 = emptyList() private var nextPages = Array(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) } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt index 115c2c2de..8e74fc990 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt @@ -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() - - private var nextPage: String? = null - - private lateinit var searchAdapter: SearchAdapter - private var searchFilter = "all" + private val viewModel by viewModels() 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,80 +74,28 @@ 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() - } - } - } - - 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) { + searchResultsAdapter.loadStateFlow.collect { + val isLoading = it.source.refresh is LoadState.Loading + binding.progress.isVisible = isLoading + binding.searchResultsLayout.isGone = isLoading } - } 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() - } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.searchResultsFlow.collectLatest { + searchResultsAdapter.submitData(it) } - } catch (e: Exception) { - Log.e(TAG(), e.toString()) - return@launch - } - nextPage = response.nextpage - if (response.items.isNotEmpty()) { - searchAdapter.submitList(searchAdapter.currentList + response.items) } } } diff --git a/app/src/main/java/com/github/libretube/ui/models/SearchResultViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/SearchResultViewModel.kt new file mode 100644 index 000000000..4cc9be4ed --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/models/SearchResultViewModel.kt @@ -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 + } +} diff --git a/app/src/main/java/com/github/libretube/ui/models/sources/SearchPagingSource.kt b/app/src/main/java/com/github/libretube/ui/models/sources/SearchPagingSource.kt new file mode 100644 index 000000000..fd9a97cee --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/models/sources/SearchPagingSource.kt @@ -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() { + override fun getRefreshKey(state: PagingState) = null + + override suspend fun load(params: LoadParams): LoadResult { + 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) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 352f997db..b61d1c23d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }