From f03830c62f15c9746657893497e7988b1fb21137 Mon Sep 17 00:00:00 2001 From: MohitMali Date: Tue, 1 Aug 2023 17:55:01 +0530 Subject: [PATCH] Introduced an endless suggestion list in search. * With the integration of libkiwix12, we now receive full results based on the search term. Previously, we were loading the entire list, but we have now implemented pagination to enhance the search functionality. * The libkiwix provides us with a Search object, which enables us to obtain the suggestion list using start and end indices. Consequently, we have modified our `ZimSearchResultGenerator` code to return the `Search` object instead of a list. * To accommodate the changes, we have updated the return type of `SearchResultGenerator` to a nullable Search. This change is necessary because we initialize the `SearchState` when the search is initialized in `SearchViewModel`, and initially, we do not have the Search object available. The nullable return type allows us to pass the Search object when it becomes available. --- core/detekt_baseline.xml | 1 + .../core/base/adapter/BaseDelegateAdapter.kt | 7 +++ .../kiwixmobile/core/reader/ZimFileReader.kt | 14 ------ .../kiwixmobile/core/search/SearchFragment.kt | 47 ++++++++++++++++++- .../search/viewmodel/SearchResultGenerator.kt | 10 ++-- .../core/search/viewmodel/SearchState.kt | 24 ++++++++-- .../core/search/viewmodel/SearchViewModel.kt | 13 ++++- .../core/search/viewmodel/SearchStateTest.kt | 4 +- 8 files changed, 89 insertions(+), 31 deletions(-) diff --git a/core/detekt_baseline.xml b/core/detekt_baseline.xml index 2005344b5..74bf8b959 100644 --- a/core/detekt_baseline.xml +++ b/core/detekt_baseline.xml @@ -18,6 +18,7 @@ MagicNumber:ArticleCount.kt$ArticleCount$3 MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100 MagicNumber:DownloadItem.kt$DownloadItem$1000L + MagicNumber:SearchState.kt$SearchState$100 MagicNumber:DownloaderModule.kt$DownloaderModule$5 MagicNumber:FetchDownloadNotificationManager.kt$FetchDownloadNotificationManager$100 MagicNumber:FetchDownloadRequester.kt$10 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/BaseDelegateAdapter.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/BaseDelegateAdapter.kt index cae0ea615..7b7162445 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/BaseDelegateAdapter.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/BaseDelegateAdapter.kt @@ -37,6 +37,13 @@ abstract class BaseDelegateAdapter( notifyDataSetChanged() } + // Function to add new data to the adapter + fun addData(newData: List) { + val startPosition = items.size + items = items.toMutableList().apply { addAll(newData) } + notifyItemRangeInserted(startPosition, newData.size) + } + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt index 6171342ee..2f9979288 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt @@ -32,7 +32,6 @@ import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS import org.kiwix.kiwixmobile.core.main.UNINITIALISE_HTML import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem import org.kiwix.kiwixmobile.core.utils.files.FileUtils import org.kiwix.libkiwix.JNIKiwixException import org.kiwix.libzim.Archive @@ -143,19 +142,6 @@ class ZimFileReader constructor( null } - fun getSearchResultList(search: Search?): List { - val suggestionList = mutableListOf() - val suggestionIterator = - search?.getResults(0, search.estimatedMatches.toInt()) - suggestionIterator?.let { - while (it.hasNext()) { - val entry = it.next() - suggestionList.add(ZimSearchResultListItem(entry.title)) - } - } - return suggestionList - } - fun getPageUrlFrom(title: String): String? = if (jniKiwixReader.hasEntryByTitle(title)) jniKiwixReader.getEntryByTitle(title).path diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt index fada01a05..8cfdb5959 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt @@ -65,6 +65,7 @@ import org.kiwix.kiwixmobile.core.utils.SimpleTextListener import javax.inject.Inject const val NAV_ARG_SEARCH_STRING = "searchString" +const val LAST_VISIBLE_ITEM = 5 class SearchFragment : BaseFragment() { @@ -76,6 +77,8 @@ class SearchFragment : BaseFragment() { private val searchViewModel by lazy { viewModel(viewModelFactory) } private var searchAdapter: SearchAdapter? = null + private var isLoading = false + private var searchState: SearchState? = null override fun inject(baseActivity: BaseActivity) { baseActivity.cachedComponent.inject(this) @@ -104,6 +107,22 @@ class SearchFragment : BaseFragment() { adapter = searchAdapter layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) setHasFixedSize(true) + // Add scroll listener to detect when the last item is reached + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val totalItemCount = layoutManager.itemCount + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + + // Check if the user is about to reach the last item + if (!isLoading && totalItemCount <= (lastVisibleItem + LAST_VISIBLE_ITEM)) { + // Load more data when the last item is almost visible + loadMoreSearchResult() + } + } + }) } lifecycleScope.launchWhenCreated { searchViewModel.effects.collect { it.invokeWith(this@SearchFragment.coreMainActivity) } @@ -111,6 +130,27 @@ class SearchFragment : BaseFragment() { handleBackPress() } + private fun loadMoreSearchResult() { + if (isLoading) { + return + } + val startIndex = searchAdapter?.itemCount ?: 0 + // Set isLoading flag to true to prevent multiple load requests at once + isLoading = true + val fetchMoreSearchResults = searchState?.getVisibleResults(startIndex) + if (fetchMoreSearchResults?.isNotEmpty() == true) { + // Check if there is no duplicate entry, this is specially added for searched history items. + val nonDuplicateResults = fetchMoreSearchResults.filter { newItem -> + searchAdapter?.items?.any { it != newItem } == true + } + + // Append new data (non-duplicate items) to the existing dataset + searchAdapter?.addData(nonDuplicateResults) + + isLoading = false // Set isLoading to false when data is loaded successfully + } + } + private fun handleBackPress() { activity?.onBackPressedDispatcher?.addCallback( viewLifecycleOwner, @@ -136,6 +176,7 @@ class SearchFragment : BaseFragment() { override fun onDestroyView() { super.onDestroyView() + searchState = null activity?.intent?.action = null searchView = null searchInTextMenuItem = null @@ -194,11 +235,13 @@ class SearchFragment : BaseFragment() { } private fun render(state: SearchState) { + searchState = state searchInTextMenuItem?.isVisible = state.searchOrigin == FromWebView searchInTextMenuItem?.isEnabled = state.searchTerm.isNotBlank() fragmentSearchBinding?.searchLoadingIndicator?.isShowing(state.isLoading) - fragmentSearchBinding?.searchNoResults?.isVisible = state.visibleResults.isEmpty() - searchAdapter?.items = state.visibleResults + fragmentSearchBinding?.searchNoResults?.isVisible = + state.getVisibleResults(0).isEmpty() + searchAdapter?.items = state.getVisibleResults(0) } private fun onItemClick(it: SearchListItem) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt index d6e4fd2fb..65d2b2980 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt @@ -22,14 +22,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import org.kiwix.kiwixmobile.core.reader.ZimFileReader -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.libzim.Search import javax.inject.Inject interface SearchResultGenerator { suspend fun generateSearchResults( searchTerm: String, zimFileReader: ZimFileReader? - ): List + ): Search? } class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator { @@ -37,7 +37,7 @@ class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator { override suspend fun generateSearchResults(searchTerm: String, zimFileReader: ZimFileReader?) = withContext(Dispatchers.IO) { if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimFileReader) - else emptyList() + else null } private suspend fun readResultsFromZim( @@ -46,8 +46,4 @@ class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator { ) = reader.also { yield() } ?.searchSuggestions(searchTerm) - .also { yield() } - .run { - reader?.getSearchResultList(this) ?: emptyList() - } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt index c0df6bb9e..2559562b8 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt @@ -26,10 +26,26 @@ data class SearchState( val recentResults: List, val searchOrigin: SearchOrigin ) { - val visibleResults = when { - searchTerm.isNotEmpty() -> searchResultsWithTerm.results - else -> recentResults - } + fun getVisibleResults(startIndex: Int): List = + if (searchTerm.isEmpty()) { + recentResults + } else { + searchResultsWithTerm.search?.let { + val maximumResults = it.estimatedMatches + val safeEndIndex = + if ((startIndex + 100) < maximumResults) startIndex + 100 else maximumResults + val searchIterator = + it.getResults(startIndex, safeEndIndex.toInt()) + val searchResults = mutableListOf() + while (searchIterator.hasNext()) { + val entry = searchIterator.next() + searchResults.add(SearchListItem.RecentSearchListItem(entry.title)) + } + searchResults + } ?: kotlin.run { + recentResults + } + } val isLoading = searchTerm != searchResultsWithTerm.searchTerm } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt index 22a9c08cb..6b0b62c2e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt @@ -59,6 +59,7 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchInPreviousScree import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowDeleteSearchDialog import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput +import org.kiwix.libzim.Search import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @@ -69,7 +70,15 @@ class SearchViewModel @Inject constructor( ) : ViewModel() { private val initialState: SearchState = - SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView) + SearchState( + "", + SearchResultsWithTerm( + "", + null + ), + emptyList(), + FromWebView + ) val state: MutableStateFlow = MutableStateFlow(initialState) private val _effects = Channel>() val effects = _effects.receiveAsFlow() @@ -165,4 +174,4 @@ class SearchViewModel @Inject constructor( } } -data class SearchResultsWithTerm(val searchTerm: String, val results: List) +data class SearchResultsWithTerm(val searchTerm: String, val search: Search?) diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt index b34b70696..7a8f0b4fe 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt @@ -35,7 +35,7 @@ internal class SearchStateTest { SearchResultsWithTerm("", results), emptyList(), FromWebView - ).visibleResults + ).getVisibleResults(0) ).isEqualTo(results) } @@ -48,7 +48,7 @@ internal class SearchStateTest { SearchResultsWithTerm("", emptyList()), results, FromWebView - ).visibleResults + ).getVisibleResults(0) ).isEqualTo(results) }