Skip to content

Commit 75c2350

Browse files
Introduced similarly spelled suggestions results.
1 parent 5142ea1 commit 75c2350

File tree

7 files changed

+163
-10
lines changed

7 files changed

+163
-10
lines changed

core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ class SearchFragment : BaseFragment() {
102102
onLoadMore = { loadMoreSearchResult() },
103103
onKeyboardSubmitButtonClick = {
104104
getSearchListItemForQuery(it)?.let(::onItemClick)
105-
}
105+
},
106+
suggestedSpelledWord = null,
107+
onSuggestedItemClick = { onSuggestedItemClick(it) }
106108
)
107109
)
108110

@@ -247,6 +249,11 @@ class SearchFragment : BaseFragment() {
247249
searchViewModel.searchResults(searchText.trim())
248250
}
249251

252+
private fun onSuggestedItemClick(suggestedText: String) {
253+
searchScreenState.update { copy(suggestedSpelledWord = null) }
254+
onSearchValueChanged(suggestedText)
255+
}
256+
250257
private fun actionMenuItems() = listOfNotNull(
251258
// Check if the `FIND_IN_PAGE` is visible or not.
252259
// If visible then show it in menu.
@@ -304,7 +311,18 @@ class SearchFragment : BaseFragment() {
304311
"Error in getting searched result\nOriginal exception ${ignore.message}"
305312
)
306313
} finally {
307-
searchScreenState.update { copy(isLoading = false) }
314+
val onlyRecentSearches =
315+
searchScreenState.value.searchList.all { it is SearchListItem.RecentSearchListItem }
316+
searchScreenState.update {
317+
copy(
318+
isLoading = false,
319+
suggestedSpelledWord = if (onlyRecentSearches && searchScreenState.value.searchText.isNotEmpty()) {
320+
"Demo"
321+
} else {
322+
null
323+
}
324+
)
325+
}
308326
}
309327
}
310328
}

core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ fun SearchScreen(
9999
onValueChange = searchScreenState.onSearchViewValueChange,
100100
onClearClick = searchScreenState.onSearchViewClearClick,
101101
modifier = Modifier,
102-
onKeyboardSubmitButtonClick = searchScreenState.onKeyboardSubmitButtonClick
102+
onKeyboardSubmitButtonClick = searchScreenState.onKeyboardSubmitButtonClick,
103+
suggestionText = searchScreenState.suggestedSpelledWord,
104+
onSuggestedWordClick = searchScreenState.onSuggestedItemClick
103105
)
104106
}
105107
)

core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,14 @@ data class SearchScreenState(
6868
/**
6969
* Manages the navigationIcon shown in the toolbar.
7070
*/
71-
val navigationIcon: @Composable() () -> Unit
71+
val navigationIcon: @Composable() () -> Unit,
72+
/**
73+
* Manages the showing of suggested word provided by the libkiwix if no search
74+
* result found for typed value.
75+
*/
76+
val suggestedSpelledWord: String?,
77+
/**
78+
* Manages the click of suggested item by the libkiwix.
79+
*/
80+
val onSuggestedItemClick: (String) -> Unit
7281
)

core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ data class SearchState(
3838
startIndex: Int,
3939
job: Job? = null,
4040
ioDispatcher: CoroutineDispatcher = Dispatchers.IO
41-
): List<SearchListItem.RecentSearchListItem>? {
41+
): List<SearchListItem>? {
4242
if (searchTerm.isEmpty()) return recentResults
4343
return searchResultsWithTerm.searchMutex.withLock {
4444
searchResultsWithTerm.suggestionSearch?.let {
@@ -57,8 +57,8 @@ data class SearchState(
5757
suggestionSearch: SuggestionSearch,
5858
startIndex: Int,
5959
job: Job?
60-
): List<SearchListItem.RecentSearchListItem>? {
61-
val results = mutableListOf<SearchListItem.RecentSearchListItem>()
60+
): List<SearchListItem.ZimSearchResultListItem>? {
61+
val results = mutableListOf<SearchListItem.ZimSearchResultListItem>()
6262

6363
// if the previous job is cancel then do not execute the code
6464
if (job?.isActive == false) return results
@@ -73,7 +73,7 @@ data class SearchState(
7373
if (job?.isActive == false) break
7474
yield()
7575
val entry = searchIterator.next()
76-
results.add(SearchListItem.RecentSearchListItem(entry.title, entry.path))
76+
results.add(SearchListItem.ZimSearchResultListItem(entry.title, entry.path))
7777
}
7878
}.onFailure {
7979
Log.e(

core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ class SearchViewModel @Inject constructor(
233233
suspend fun loadMoreSearchResults(
234234
startIndex: Int,
235235
existingSearchList: List<SearchListItem>?
236-
): List<SearchListItem.RecentSearchListItem>? {
236+
): List<SearchListItem>? {
237237
val searchResults = state.value.getVisibleResults(startIndex)
238238

239239
return searchResults?.filter { newItem ->

core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@
1818

1919
package org.kiwix.kiwixmobile.core.ui.components
2020

21+
import androidx.compose.foundation.background
22+
import androidx.compose.foundation.clickable
2123
import androidx.compose.foundation.isSystemInDarkTheme
24+
import androidx.compose.foundation.layout.Column
25+
import androidx.compose.foundation.layout.Row
26+
import androidx.compose.foundation.layout.fillMaxWidth
27+
import androidx.compose.foundation.layout.heightIn
28+
import androidx.compose.foundation.layout.padding
2229
import androidx.compose.foundation.text.KeyboardActions
2330
import androidx.compose.foundation.text.KeyboardOptions
2431
import androidx.compose.material3.Icon
@@ -29,22 +36,42 @@ import androidx.compose.material3.TextField
2936
import androidx.compose.material3.TextFieldDefaults
3037
import androidx.compose.material3.minimumInteractiveComponentSize
3138
import androidx.compose.runtime.Composable
39+
import androidx.compose.runtime.MutableState
3240
import androidx.compose.runtime.SideEffect
41+
import androidx.compose.runtime.mutableStateOf
42+
import androidx.compose.runtime.remember
43+
import androidx.compose.ui.Alignment
3344
import androidx.compose.ui.Modifier
3445
import androidx.compose.ui.focus.FocusRequester
3546
import androidx.compose.ui.focus.focusRequester
47+
import androidx.compose.ui.geometry.Rect
3648
import androidx.compose.ui.graphics.Color
49+
import androidx.compose.ui.layout.onGloballyPositioned
50+
import androidx.compose.ui.layout.positionInRoot
3751
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
3852
import androidx.compose.ui.platform.testTag
3953
import androidx.compose.ui.res.painterResource
4054
import androidx.compose.ui.res.stringResource
4155
import androidx.compose.ui.semantics.contentDescription
4256
import androidx.compose.ui.semantics.semantics
57+
import androidx.compose.ui.text.AnnotatedString
58+
import androidx.compose.ui.text.SpanStyle
4359
import androidx.compose.ui.text.TextStyle
60+
import androidx.compose.ui.text.buildAnnotatedString
61+
import androidx.compose.ui.text.font.FontWeight
4462
import androidx.compose.ui.text.input.ImeAction
4563
import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
64+
import androidx.compose.ui.text.withStyle
65+
import androidx.compose.ui.unit.IntOffset
66+
import androidx.compose.ui.unit.toSize
67+
import androidx.compose.ui.window.Popup
4668
import org.kiwix.kiwixmobile.core.R
69+
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
4770
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
71+
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
72+
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.MINIMUM_HEIGHT_OF_SEARCH_ITEM
73+
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SEARCH_ITEM_TEXT_SIZE
74+
import kotlin.math.roundToInt
4875

4976
@Composable
5077
fun KiwixSearchView(
@@ -53,9 +80,94 @@ fun KiwixSearchView(
5380
placeholder: String = stringResource(R.string.search_label),
5481
searchViewTextFiledTestTag: String = "",
5582
clearButtonTestTag: String = "",
83+
suggestionText: String? = null,
84+
onSuggestedWordClick: (String) -> Unit = {},
5685
onValueChange: (String) -> Unit,
5786
onClearClick: () -> Unit,
5887
onKeyboardSubmitButtonClick: (String) -> Unit = {}
88+
) {
89+
val textFieldBounds = remember { mutableStateOf(Rect.Zero) }
90+
Column {
91+
SearchViewTextFiled(
92+
modifier,
93+
searchViewTextFiledTestTag,
94+
value,
95+
placeholder,
96+
onValueChange,
97+
onClearClick,
98+
clearButtonTestTag,
99+
onKeyboardSubmitButtonClick,
100+
textFieldBounds
101+
)
102+
ShowCorrectWordSuggestion(suggestionText, onSuggestedWordClick, textFieldBounds)
103+
}
104+
}
105+
106+
@Composable
107+
private fun ShowCorrectWordSuggestion(
108+
suggestionText: String?,
109+
onSuggestedWordClick: (String) -> Unit,
110+
textFieldBounds: MutableState<Rect>
111+
) {
112+
if (suggestionText.isNullOrBlank()) return
113+
Popup(
114+
alignment = Alignment.TopStart,
115+
offset = IntOffset(
116+
x = textFieldBounds.value.left.roundToInt(),
117+
y = textFieldBounds.value.bottom.roundToInt()
118+
)
119+
) {
120+
Row(
121+
modifier = Modifier
122+
.fillMaxWidth()
123+
.heightIn(min = MINIMUM_HEIGHT_OF_SEARCH_ITEM)
124+
.background(MaterialTheme.colorScheme.background)
125+
.clickable { onSuggestedWordClick(suggestionText) },
126+
verticalAlignment = Alignment.CenterVertically
127+
) {
128+
Text(
129+
text = getSuggestedHighlightedText(suggestionText),
130+
modifier = Modifier
131+
.padding(horizontal = EIGHT_DP)
132+
.weight(1f),
133+
fontSize = SEARCH_ITEM_TEXT_SIZE
134+
)
135+
Icon(
136+
painter = painterResource(id = R.drawable.ic_open_in_new_24dp),
137+
contentDescription = stringResource(id = R.string.suggested_search_icon_description),
138+
modifier = Modifier.padding(horizontal = EIGHT_DP)
139+
)
140+
}
141+
}
142+
}
143+
144+
@Composable
145+
private fun getSuggestedHighlightedText(suggestionText: String): AnnotatedString {
146+
val rawString = stringResource(R.string.suggest_search_text)
147+
val parts = rawString.split("%s")
148+
val before = parts.getOrNull(ZERO).orEmpty()
149+
val after = parts.getOrNull(ONE).orEmpty()
150+
return buildAnnotatedString {
151+
append(before)
152+
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
153+
append(suggestionText)
154+
}
155+
append(after)
156+
}
157+
}
158+
159+
@Suppress("LongParameterList", "LongMethod")
160+
@Composable
161+
private fun SearchViewTextFiled(
162+
modifier: Modifier,
163+
searchViewTextFiledTestTag: String,
164+
value: String,
165+
placeholder: String,
166+
onValueChange: (String) -> Unit,
167+
onClearClick: () -> Unit,
168+
clearButtonTestTag: String,
169+
onKeyboardSubmitButtonClick: (String) -> Unit,
170+
textFieldBounds: MutableState<Rect>
59171
) {
60172
val hintColor = if (isSystemInDarkTheme()) {
61173
Color.LightGray
@@ -78,7 +190,17 @@ fun KiwixSearchView(
78190
.testTag(searchViewTextFiledTestTag)
79191
.minimumInteractiveComponentSize()
80192
.focusRequester(focusRequester)
81-
.semantics { contentDescription = placeholder },
193+
.semantics { contentDescription = placeholder }
194+
.onGloballyPositioned { coordinates ->
195+
val position = coordinates.positionInRoot()
196+
val size = coordinates.size.toSize()
197+
textFieldBounds.value = Rect(
198+
position.x,
199+
position.y,
200+
position.x + size.width,
201+
position.y + size.height
202+
)
203+
},
82204
singleLine = true,
83205
value = value,
84206
placeholder = {

core/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,8 @@
412412
<string name="downloading_state">Downloading</string>
413413
<string name="download_failed_state">Failed</string>
414414
<string name="searchview_description_clear">Clear query</string>
415+
<string name="suggest_search_text">Do you mean %s?</string>
416+
<string name="suggested_search_icon_description">Tap to use this suggestion and automatically correct your search</string>
415417
<string name="file_system_scan_dialog_title">Scan storage for books?</string>
416418
<string name="file_system_scan_dialog_message">Kiwix will scan your device storage and list the available books here.</string>
417419
</resources>

0 commit comments

Comments
 (0)