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)
}