mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 18:56:44 -04:00
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.
This commit is contained in:
parent
a99a988a98
commit
f03830c62f
@ -18,6 +18,7 @@
|
|||||||
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
|
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
|
||||||
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>
|
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>
|
||||||
<ID>MagicNumber:DownloadItem.kt$DownloadItem$1000L</ID>
|
<ID>MagicNumber:DownloadItem.kt$DownloadItem$1000L</ID>
|
||||||
|
<ID>MagicNumber:SearchState.kt$SearchState$100</ID>
|
||||||
<ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID>
|
<ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID>
|
||||||
<ID>MagicNumber:FetchDownloadNotificationManager.kt$FetchDownloadNotificationManager$100</ID>
|
<ID>MagicNumber:FetchDownloadNotificationManager.kt$FetchDownloadNotificationManager$100</ID>
|
||||||
<ID>MagicNumber:FetchDownloadRequester.kt$10</ID>
|
<ID>MagicNumber:FetchDownloadRequester.kt$10</ID>
|
||||||
|
@ -37,6 +37,13 @@ abstract class BaseDelegateAdapter<ITEM>(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to add new data to the adapter
|
||||||
|
fun addData(newData: List<ITEM>) {
|
||||||
|
val startPosition = items.size
|
||||||
|
items = items.toMutableList().apply { addAll(newData) }
|
||||||
|
notifyItemRangeInserted(startPosition, newData.size)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int
|
viewType: Int
|
||||||
|
@ -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.UNINITIALISER_ADDRESS
|
||||||
import org.kiwix.kiwixmobile.core.main.UNINITIALISE_HTML
|
import org.kiwix.kiwixmobile.core.main.UNINITIALISE_HTML
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX
|
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.kiwixmobile.core.utils.files.FileUtils
|
||||||
import org.kiwix.libkiwix.JNIKiwixException
|
import org.kiwix.libkiwix.JNIKiwixException
|
||||||
import org.kiwix.libzim.Archive
|
import org.kiwix.libzim.Archive
|
||||||
@ -143,19 +142,6 @@ class ZimFileReader constructor(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSearchResultList(search: Search?): List<ZimSearchResultListItem> {
|
|
||||||
val suggestionList = mutableListOf<ZimSearchResultListItem>()
|
|
||||||
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? =
|
fun getPageUrlFrom(title: String): String? =
|
||||||
if (jniKiwixReader.hasEntryByTitle(title))
|
if (jniKiwixReader.hasEntryByTitle(title))
|
||||||
jniKiwixReader.getEntryByTitle(title).path
|
jniKiwixReader.getEntryByTitle(title).path
|
||||||
|
@ -65,6 +65,7 @@ import org.kiwix.kiwixmobile.core.utils.SimpleTextListener
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val NAV_ARG_SEARCH_STRING = "searchString"
|
const val NAV_ARG_SEARCH_STRING = "searchString"
|
||||||
|
const val LAST_VISIBLE_ITEM = 5
|
||||||
|
|
||||||
class SearchFragment : BaseFragment() {
|
class SearchFragment : BaseFragment() {
|
||||||
|
|
||||||
@ -76,6 +77,8 @@ class SearchFragment : BaseFragment() {
|
|||||||
|
|
||||||
private val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
|
private val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
|
||||||
private var searchAdapter: SearchAdapter? = null
|
private var searchAdapter: SearchAdapter? = null
|
||||||
|
private var isLoading = false
|
||||||
|
private var searchState: SearchState? = null
|
||||||
|
|
||||||
override fun inject(baseActivity: BaseActivity) {
|
override fun inject(baseActivity: BaseActivity) {
|
||||||
baseActivity.cachedComponent.inject(this)
|
baseActivity.cachedComponent.inject(this)
|
||||||
@ -104,6 +107,22 @@ class SearchFragment : BaseFragment() {
|
|||||||
adapter = searchAdapter
|
adapter = searchAdapter
|
||||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
setHasFixedSize(true)
|
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 {
|
lifecycleScope.launchWhenCreated {
|
||||||
searchViewModel.effects.collect { it.invokeWith(this@SearchFragment.coreMainActivity) }
|
searchViewModel.effects.collect { it.invokeWith(this@SearchFragment.coreMainActivity) }
|
||||||
@ -111,6 +130,27 @@ class SearchFragment : BaseFragment() {
|
|||||||
handleBackPress()
|
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() {
|
private fun handleBackPress() {
|
||||||
activity?.onBackPressedDispatcher?.addCallback(
|
activity?.onBackPressedDispatcher?.addCallback(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
@ -136,6 +176,7 @@ class SearchFragment : BaseFragment() {
|
|||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
searchState = null
|
||||||
activity?.intent?.action = null
|
activity?.intent?.action = null
|
||||||
searchView = null
|
searchView = null
|
||||||
searchInTextMenuItem = null
|
searchInTextMenuItem = null
|
||||||
@ -194,11 +235,13 @@ class SearchFragment : BaseFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun render(state: SearchState) {
|
private fun render(state: SearchState) {
|
||||||
|
searchState = state
|
||||||
searchInTextMenuItem?.isVisible = state.searchOrigin == FromWebView
|
searchInTextMenuItem?.isVisible = state.searchOrigin == FromWebView
|
||||||
searchInTextMenuItem?.isEnabled = state.searchTerm.isNotBlank()
|
searchInTextMenuItem?.isEnabled = state.searchTerm.isNotBlank()
|
||||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(state.isLoading)
|
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(state.isLoading)
|
||||||
fragmentSearchBinding?.searchNoResults?.isVisible = state.visibleResults.isEmpty()
|
fragmentSearchBinding?.searchNoResults?.isVisible =
|
||||||
searchAdapter?.items = state.visibleResults
|
state.getVisibleResults(0).isEmpty()
|
||||||
|
searchAdapter?.items = state.getVisibleResults(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemClick(it: SearchListItem) {
|
private fun onItemClick(it: SearchListItem) {
|
||||||
|
@ -22,14 +22,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.libzim.Search
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface SearchResultGenerator {
|
interface SearchResultGenerator {
|
||||||
suspend fun generateSearchResults(
|
suspend fun generateSearchResults(
|
||||||
searchTerm: String,
|
searchTerm: String,
|
||||||
zimFileReader: ZimFileReader?
|
zimFileReader: ZimFileReader?
|
||||||
): List<SearchListItem>
|
): Search?
|
||||||
}
|
}
|
||||||
|
|
||||||
class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
|
class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
|
||||||
@ -37,7 +37,7 @@ class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
|
|||||||
override suspend fun generateSearchResults(searchTerm: String, zimFileReader: ZimFileReader?) =
|
override suspend fun generateSearchResults(searchTerm: String, zimFileReader: ZimFileReader?) =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimFileReader)
|
if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimFileReader)
|
||||||
else emptyList()
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun readResultsFromZim(
|
private suspend fun readResultsFromZim(
|
||||||
@ -46,8 +46,4 @@ class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
|
|||||||
) =
|
) =
|
||||||
reader.also { yield() }
|
reader.also { yield() }
|
||||||
?.searchSuggestions(searchTerm)
|
?.searchSuggestions(searchTerm)
|
||||||
.also { yield() }
|
|
||||||
.run {
|
|
||||||
reader?.getSearchResultList(this) ?: emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,25 @@ data class SearchState(
|
|||||||
val recentResults: List<SearchListItem.RecentSearchListItem>,
|
val recentResults: List<SearchListItem.RecentSearchListItem>,
|
||||||
val searchOrigin: SearchOrigin
|
val searchOrigin: SearchOrigin
|
||||||
) {
|
) {
|
||||||
val visibleResults = when {
|
fun getVisibleResults(startIndex: Int): List<SearchListItem.RecentSearchListItem> =
|
||||||
searchTerm.isNotEmpty() -> searchResultsWithTerm.results
|
if (searchTerm.isEmpty()) {
|
||||||
else -> recentResults
|
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<SearchListItem.RecentSearchListItem>()
|
||||||
|
while (searchIterator.hasNext()) {
|
||||||
|
val entry = searchIterator.next()
|
||||||
|
searchResults.add(SearchListItem.RecentSearchListItem(entry.title))
|
||||||
|
}
|
||||||
|
searchResults
|
||||||
|
} ?: kotlin.run {
|
||||||
|
recentResults
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLoading = searchTerm != searchResultsWithTerm.searchTerm
|
val isLoading = searchTerm != searchResultsWithTerm.searchTerm
|
||||||
|
@ -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.ShowDeleteSearchDialog
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast
|
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput
|
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput
|
||||||
|
import org.kiwix.libzim.Search
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@ -69,7 +70,15 @@ class SearchViewModel @Inject constructor(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val initialState: SearchState =
|
private val initialState: SearchState =
|
||||||
SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView)
|
SearchState(
|
||||||
|
"",
|
||||||
|
SearchResultsWithTerm(
|
||||||
|
"",
|
||||||
|
null
|
||||||
|
),
|
||||||
|
emptyList(),
|
||||||
|
FromWebView
|
||||||
|
)
|
||||||
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
||||||
private val _effects = Channel<SideEffect<*>>()
|
private val _effects = Channel<SideEffect<*>>()
|
||||||
val effects = _effects.receiveAsFlow()
|
val effects = _effects.receiveAsFlow()
|
||||||
@ -165,4 +174,4 @@ class SearchViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SearchResultsWithTerm(val searchTerm: String, val results: List<SearchListItem>)
|
data class SearchResultsWithTerm(val searchTerm: String, val search: Search?)
|
||||||
|
@ -35,7 +35,7 @@ internal class SearchStateTest {
|
|||||||
SearchResultsWithTerm("", results),
|
SearchResultsWithTerm("", results),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
FromWebView
|
FromWebView
|
||||||
).visibleResults
|
).getVisibleResults(0)
|
||||||
).isEqualTo(results)
|
).isEqualTo(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ internal class SearchStateTest {
|
|||||||
SearchResultsWithTerm("", emptyList()),
|
SearchResultsWithTerm("", emptyList()),
|
||||||
results,
|
results,
|
||||||
FromWebView
|
FromWebView
|
||||||
).visibleResults
|
).getVisibleResults(0)
|
||||||
).isEqualTo(results)
|
).isEqualTo(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user