mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
Merge pull request #4329 from kiwix/Fixes#4246
Migrated `SearchFragment` to jetpack compose.
This commit is contained in:
commit
ad62661e67
@ -31,6 +31,7 @@ import org.kiwix.kiwixmobile.BaseRobot
|
||||
import org.kiwix.kiwixmobile.Findable.ViewId
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.language.composables.LANGUAGE_ITEM_CHECKBOX_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.nav.destination.library.online.LANGUAGE_MENU_ICON_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.testutils.TestUtils
|
||||
|
@ -18,6 +18,7 @@
|
||||
package org.kiwix.kiwixmobile.search
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@ -25,7 +26,6 @@ import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.accessibility.AccessibilityChecks
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
@ -47,11 +47,11 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.kiwix.kiwixmobile.BaseActivityTest
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.core.R.id
|
||||
import org.kiwix.kiwixmobile.core.search.SearchFragment
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER
|
||||
import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER
|
||||
import org.kiwix.kiwixmobile.main.KiwixMainActivity
|
||||
import org.kiwix.kiwixmobile.nav.destination.library.local.LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader
|
||||
@ -73,6 +73,9 @@ class SearchFragmentTest : BaseActivityTest() {
|
||||
@JvmField
|
||||
val retryRule = RetryRule()
|
||||
|
||||
@get:Rule(order = COMPOSE_TEST_RULE_ORDER)
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private lateinit var kiwixMainActivity: KiwixMainActivity
|
||||
private lateinit var uiDevice: UiDevice
|
||||
private lateinit var downloadingZimFile: File
|
||||
@ -115,10 +118,6 @@ class SearchFragmentTest : BaseActivityTest() {
|
||||
setRunChecksFromRootView(true)
|
||||
setSuppressingResultMatcher(
|
||||
anyOf(
|
||||
allOf(
|
||||
matchesCheck(TouchTargetSizeCheck::class.java),
|
||||
matchesViews(ViewMatchers.withId(id.menu_searchintext))
|
||||
),
|
||||
allOf(
|
||||
matchesCheck(TouchTargetSizeCheck::class.java),
|
||||
matchesViews(
|
||||
@ -142,28 +141,32 @@ class SearchFragmentTest : BaseActivityTest() {
|
||||
search { checkZimFileSearchSuccessful(R.id.readerFragment) }
|
||||
openSearchWithQuery("Android", testZimFile)
|
||||
search {
|
||||
clickOnSearchItemInSearchList()
|
||||
clickOnSearchItemInSearchList(composeTestRule)
|
||||
checkZimFileSearchSuccessful(R.id.readerFragment)
|
||||
}
|
||||
|
||||
openSearchWithQuery(zimFile = testZimFile)
|
||||
search {
|
||||
// test with fast typing/deleting
|
||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery)
|
||||
assertSearchSuccessful(searchUnitTestResult)
|
||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice)
|
||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery, composeTestRule = composeTestRule)
|
||||
assertSearchSuccessful(searchUnitTestResult, composeTestRule)
|
||||
deleteSearchedQueryFrequently(
|
||||
searchUnitTestingQuery,
|
||||
uiDevice,
|
||||
composeTestRule = composeTestRule
|
||||
)
|
||||
|
||||
// test with a short delay typing/deleting to
|
||||
// properly test the cancelling of previously searching task
|
||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50)
|
||||
assertSearchSuccessful(searchUnitTestResult)
|
||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50)
|
||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50, composeTestRule)
|
||||
assertSearchSuccessful(searchUnitTestResult, composeTestRule)
|
||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50, composeTestRule)
|
||||
|
||||
// test with a long delay typing/deleting to
|
||||
// properly execute the search query letter by letter
|
||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300)
|
||||
assertSearchSuccessful(searchUnitTestResult)
|
||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300)
|
||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300, composeTestRule)
|
||||
assertSearchSuccessful(searchUnitTestResult, composeTestRule)
|
||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300, composeTestRule)
|
||||
// to close the keyboard
|
||||
pressBack()
|
||||
// go to reader screen
|
||||
@ -190,39 +193,46 @@ class SearchFragmentTest : BaseActivityTest() {
|
||||
openSearchWithQuery(zimFile = downloadingZimFile)
|
||||
search {
|
||||
// test with fast typing/deleting
|
||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile)
|
||||
assertSearchSuccessful(searchResultForDownloadedZimFile)
|
||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice)
|
||||
searchWithFrequentlyTypedWords(
|
||||
searchQueryForDownloadedZimFile,
|
||||
composeTestRule = composeTestRule
|
||||
)
|
||||
assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule)
|
||||
deleteSearchedQueryFrequently(
|
||||
searchQueryForDownloadedZimFile,
|
||||
uiDevice,
|
||||
composeTestRule = composeTestRule
|
||||
)
|
||||
|
||||
// test with a short delay typing/deleting to
|
||||
// properly test the cancelling of previously searching task
|
||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50)
|
||||
assertSearchSuccessful(searchResultForDownloadedZimFile)
|
||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50)
|
||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50, composeTestRule)
|
||||
assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule)
|
||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50, composeTestRule)
|
||||
|
||||
// test with a long delay typing/deleting to
|
||||
// properly execute the search query letter by letter
|
||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300)
|
||||
assertSearchSuccessful(searchResultForDownloadedZimFile)
|
||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300)
|
||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300, composeTestRule)
|
||||
assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule)
|
||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300, composeTestRule)
|
||||
// open the reader fragment for next text case.
|
||||
openKiwixReaderFragmentWithFile(downloadingZimFile)
|
||||
clickOnNavigationIcon(composeTestRule)
|
||||
}
|
||||
|
||||
// Added test for checking the crash scenario where the application was crashing when we
|
||||
// frequently searched for article, and clicked on the searched item.
|
||||
search {
|
||||
// test by searching 10 article and clicking on them
|
||||
searchAndClickOnArticle(searchQueryForDownloadedZimFile)
|
||||
searchAndClickOnArticle("A Song")
|
||||
searchAndClickOnArticle("The Ra")
|
||||
searchAndClickOnArticle("The Ge")
|
||||
searchAndClickOnArticle("Wish")
|
||||
searchAndClickOnArticle("WIFI")
|
||||
searchAndClickOnArticle("Woman")
|
||||
searchAndClickOnArticle("Big Ba")
|
||||
searchAndClickOnArticle("My Wor")
|
||||
searchAndClickOnArticle("100")
|
||||
searchAndClickOnArticle(searchQueryForDownloadedZimFile, composeTestRule)
|
||||
searchAndClickOnArticle("A Song", composeTestRule)
|
||||
searchAndClickOnArticle("The Ra", composeTestRule)
|
||||
searchAndClickOnArticle("The Ge", composeTestRule)
|
||||
searchAndClickOnArticle("Wish", composeTestRule)
|
||||
searchAndClickOnArticle("WIFI", composeTestRule)
|
||||
searchAndClickOnArticle("Woman", composeTestRule)
|
||||
searchAndClickOnArticle("Big Ba", composeTestRule)
|
||||
searchAndClickOnArticle("My Wor", composeTestRule)
|
||||
searchAndClickOnArticle("100", composeTestRule)
|
||||
assertArticleLoaded()
|
||||
}
|
||||
removeTemporaryZimFilesToFreeUpDeviceStorage()
|
||||
|
@ -19,28 +19,33 @@
|
||||
package org.kiwix.kiwixmobile.search
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.clearText
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.espresso.web.sugar.Web.onWebView
|
||||
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
|
||||
import androidx.test.espresso.web.webdriver.Locator
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import applyWithViewHierarchyPrinting
|
||||
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
|
||||
import com.adevinta.android.barista.internal.matcher.HelperMatchers.atPosition
|
||||
import org.kiwix.kiwixmobile.BaseRobot
|
||||
import org.kiwix.kiwixmobile.Findable.ViewId
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.core.ui.components.NAVIGATION_ICON_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.testutils.TestUtils
|
||||
import org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS
|
||||
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
|
||||
import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout
|
||||
|
||||
fun search(func: SearchRobot.() -> Unit) = SearchRobot().applyWithViewHierarchyPrinting(func)
|
||||
|
||||
@ -50,15 +55,11 @@ class SearchRobot : BaseRobot() {
|
||||
val searchQueryForDownloadedZimFile = "A Fool"
|
||||
val searchResultForDownloadedZimFile = "A Fool for You"
|
||||
|
||||
fun clickOnSearchItemInSearchList() {
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
isVisible(ViewId(R.id.search_list))
|
||||
onView(withId(R.id.search_list)).perform(
|
||||
actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
||||
0,
|
||||
click()
|
||||
)
|
||||
)
|
||||
fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) {
|
||||
composeTestRule.apply {
|
||||
waitUntilTimeout()
|
||||
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkZimFileSearchSuccessful(readerFragment: Int) {
|
||||
@ -66,31 +67,47 @@ class SearchRobot : BaseRobot() {
|
||||
isVisible(ViewId(readerFragment))
|
||||
}
|
||||
|
||||
fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) {
|
||||
fun searchWithFrequentlyTypedWords(
|
||||
query: String,
|
||||
wait: Long = 0L,
|
||||
composeTestRule: ComposeContentTestRule
|
||||
) {
|
||||
testFlakyView({
|
||||
val searchView = onView(withId(androidx.appcompat.R.id.search_src_text))
|
||||
for (char in query) {
|
||||
searchView.perform(typeText(char.toString()))
|
||||
if (wait != 0L) {
|
||||
BaristaSleepInteractions.sleep(wait)
|
||||
composeTestRule.apply {
|
||||
waitUntilTimeout()
|
||||
val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG)
|
||||
searchView.performTextInput("")
|
||||
for (char in query) {
|
||||
searchView.performTextInput(char.toString())
|
||||
if (wait != 0L) {
|
||||
waitUntilTimeout(wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun assertSearchSuccessful(searchResult: String) {
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
val recyclerViewId = R.id.search_list
|
||||
|
||||
onView(withId(recyclerViewId)).check(
|
||||
matches(
|
||||
atPosition(0, hasDescendant(withText(searchResult)))
|
||||
fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) {
|
||||
composeTestRule.apply {
|
||||
waitUntil(
|
||||
timeoutMillis = TEST_PAUSE_MS.toLong(),
|
||||
condition = {
|
||||
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)
|
||||
.fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
)
|
||||
)
|
||||
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0]
|
||||
.assert(hasText(searchResult))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) {
|
||||
for (i in textToDelete.indices) {
|
||||
fun deleteSearchedQueryFrequently(
|
||||
textToDelete: String,
|
||||
uiDevice: UiDevice,
|
||||
wait: Long = 0L,
|
||||
composeTestRule: ComposeContentTestRule
|
||||
) {
|
||||
repeat(textToDelete.length) {
|
||||
uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL)
|
||||
if (wait != 0L) {
|
||||
BaristaSleepInteractions.sleep(wait)
|
||||
@ -98,20 +115,22 @@ class SearchRobot : BaseRobot() {
|
||||
}
|
||||
|
||||
// clear search query if any remains due to any condition not to affect any other test scenario
|
||||
val searchView = onView(withId(androidx.appcompat.R.id.search_src_text))
|
||||
searchView.perform(clearText())
|
||||
composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance()
|
||||
}
|
||||
|
||||
fun clickOnNavigationIcon(composeTestRule: ComposeContentTestRule) {
|
||||
composeTestRule.onNodeWithTag(NAVIGATION_ICON_TESTING_TAG).performClick()
|
||||
}
|
||||
|
||||
private fun openSearchScreen() {
|
||||
testFlakyView({ onView(withId(R.id.menu_search)).perform(click()) })
|
||||
}
|
||||
|
||||
fun searchAndClickOnArticle(searchString: String) {
|
||||
// wait a bit to properly load the ZIM file in the reader
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) {
|
||||
openSearchScreen()
|
||||
searchWithFrequentlyTypedWords(searchString)
|
||||
clickOnSearchItemInSearchList()
|
||||
searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule)
|
||||
clickOnSearchItemInSearchList(composeTestRule)
|
||||
checkZimFileSearchSuccessful(org.kiwix.kiwixmobile.R.id.readerFragment)
|
||||
}
|
||||
|
||||
fun assertArticleLoaded() {
|
||||
|
@ -47,7 +47,6 @@ import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
const val SAVE_ICON_TESTING_TAG = "saveLanguages"
|
||||
const val SEARCH_FIELD_TESTING_TAG = "searchField"
|
||||
|
||||
class LanguageFragment : BaseFragment() {
|
||||
private val languageViewModel by lazy { viewModel<LanguageViewModel>(viewModelFactory) }
|
||||
|
@ -39,6 +39,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.extensions.CollectSideEffectWithActivity
|
||||
import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
|
||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
|
||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView
|
||||
|
@ -51,7 +51,6 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.kiwix.kiwixmobile.core.R.string
|
||||
import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll
|
||||
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
|
||||
@ -72,6 +71,7 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIX_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.THREE_DP
|
||||
import org.kiwix.kiwixmobile.nav.destination.library.local.rememberScrollBehavior
|
||||
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
|
||||
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
|
||||
@ -258,7 +258,7 @@ private fun ShowFetchingLibraryLayout(message: String) {
|
||||
) {
|
||||
ContentLoadingProgressBar(
|
||||
modifier = Modifier.size(DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE),
|
||||
circularProgressBarStockWidth = 3.dp,
|
||||
circularProgressBarStockWidth = THREE_DP,
|
||||
progressBarTrackColor = cardContainerColor
|
||||
)
|
||||
Text(
|
||||
|
@ -132,8 +132,7 @@
|
||||
<fragment
|
||||
android:id="@+id/searchFragment"
|
||||
android:name="org.kiwix.kiwixmobile.core.search.SearchFragment"
|
||||
android:label="SearchFragment"
|
||||
tools:layout="@layout/fragment_search">
|
||||
android:label="SearchFragment">
|
||||
<action
|
||||
android:id="@+id/action_searchFragment_to_readerFragment"
|
||||
app:destination="@id/readerFragment"
|
||||
|
@ -23,7 +23,7 @@ import io.objectbox.query.QueryBuilder
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity_
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewRecentSearchDao @Inject constructor(
|
||||
|
@ -23,7 +23,7 @@ import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchRoomEntity
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
|
||||
@Dao
|
||||
abstract class RecentSearchRoomDao {
|
||||
|
@ -17,30 +17,19 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.core.search
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.MenuItem.OnActionExpandListener
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -51,22 +40,15 @@ import kotlinx.coroutines.withContext
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||
import org.kiwix.kiwixmobile.core.base.BaseFragment
|
||||
import org.kiwix.kiwixmobile.core.databinding.FragmentSearchBinding
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
|
||||
import org.kiwix.kiwixmobile.core.extensions.closeKeyboard
|
||||
import org.kiwix.kiwixmobile.core.extensions.coreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.extensions.getDialogHostComposeView
|
||||
import org.kiwix.kiwixmobile.core.extensions.setUpSearchView
|
||||
import org.kiwix.kiwixmobile.core.extensions.update
|
||||
import org.kiwix.kiwixmobile.core.extensions.viewModel
|
||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchAdapter
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchDelegate.RecentSearchDelegate
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchDelegate.ZimSearchResultDelegate
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ActivityResultReceived
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ExitedSearch
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemClick
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemLongClick
|
||||
@ -74,35 +56,58 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnOpenInNewTabClick
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchState
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchViewModel
|
||||
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
|
||||
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
|
||||
import org.kiwix.kiwixmobile.core.utils.EXTRA_IS_WIDGET_VOICE
|
||||
import org.kiwix.kiwixmobile.core.utils.SimpleTextListener
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||
import javax.inject.Inject
|
||||
|
||||
const val NAV_ARG_SEARCH_STRING = "searchString"
|
||||
const val VISIBLE_ITEMS_THRESHOLD = 5
|
||||
const val LOADING_ITEMS_BEFORE = 3
|
||||
const val DISABLED_SEARCH_IN_TEXT_OPACITY = 0.6f
|
||||
const val ENABLED_SEARCH_IN_TEXT_OPACITY = 1f
|
||||
|
||||
class SearchFragment : BaseFragment() {
|
||||
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private var searchView: SearchView? = null
|
||||
private var searchInTextMenuItem: MenuItem? = null
|
||||
private var searchMenuItem: MenuItem? = null
|
||||
private var findInPageTextView: TextView? = null
|
||||
private var fragmentSearchBinding: FragmentSearchBinding? = null
|
||||
|
||||
@Inject lateinit var dialogShower: DialogShower
|
||||
|
||||
val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
|
||||
private var searchAdapter: SearchAdapter? = null
|
||||
private var isDataLoading = false
|
||||
private var isDataLoading = mutableStateOf(false)
|
||||
private var renderingJob: Job? = null
|
||||
|
||||
/**
|
||||
* Represents the state of the FIND_IN_PAGE menu item.
|
||||
*
|
||||
* A [Pair] containing:
|
||||
* - [Boolean]: Whether the menu item is enabled (clickable).
|
||||
* - [Boolean]: Whether the menu item is visible.
|
||||
*/
|
||||
private var findInPageMenuItem = mutableStateOf(false to false)
|
||||
private var composeView: ComposeView? = null
|
||||
private val searchScreenState = mutableStateOf(
|
||||
SearchScreenState(
|
||||
searchList = emptyList(),
|
||||
isLoading = true,
|
||||
shouldShowLoadingMoreProgressBar = false,
|
||||
searchText = "",
|
||||
onSearchViewClearClick = { onSearchClear() },
|
||||
onSearchViewValueChange = { onSearchValueChanged(it) },
|
||||
onItemClick = { onItemClick(it) },
|
||||
onNewTabIconClick = { onItemClickNewTab(it) },
|
||||
onItemLongClick = {
|
||||
searchViewModel.actions.trySend(OnItemLongClick(it)).isSuccess
|
||||
},
|
||||
navigationIcon = {
|
||||
NavigationIcon(onClick = { requireActivity().onBackPressedDispatcher.onBackPressed() })
|
||||
},
|
||||
onLoadMore = { loadMoreSearchResult() },
|
||||
onKeyboardSubmitButtonClick = {
|
||||
getSearchListItemForQuery(it)?.let(::onItemClick)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
override fun inject(baseActivity: BaseActivity) {
|
||||
baseActivity.cachedComponent.inject(this)
|
||||
}
|
||||
@ -111,80 +116,87 @@ class SearchFragment : BaseFragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
fragmentSearchBinding = FragmentSearchBinding.inflate(inflater, container, false)
|
||||
setupMenu()
|
||||
return fragmentSearchBinding?.root
|
||||
): View? = ComposeView(requireContext()).also {
|
||||
composeView = it
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
searchAdapter = SearchAdapter(
|
||||
RecentSearchDelegate(::onItemClick, ::onItemClickNewTab) {
|
||||
searchViewModel.actions.trySend(OnItemLongClick(it)).isSuccess
|
||||
},
|
||||
ZimSearchResultDelegate(::onItemClick, ::onItemClickNewTab)
|
||||
)
|
||||
setupToolbar(view)
|
||||
fragmentSearchBinding?.searchList?.run {
|
||||
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 (!isDataLoading &&
|
||||
totalItemCount <= lastVisibleItem + VISIBLE_ITEMS_THRESHOLD - LOADING_ITEMS_BEFORE
|
||||
) {
|
||||
// Load more data when the last item is almost visible
|
||||
loadMoreSearchResult()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
searchViewModel.effects.collect { it.invokeWith(this@SearchFragment.coreMainActivity) }
|
||||
composeView?.apply {
|
||||
setContent {
|
||||
SearchScreen(
|
||||
searchScreenState.value,
|
||||
actionMenuItems(),
|
||||
isDataLoading.value
|
||||
)
|
||||
DialogHost(dialogShower as AlertDialogShower)
|
||||
}
|
||||
}
|
||||
handleBackPress()
|
||||
fragmentSearchBinding?.root?.addView(
|
||||
requireContext().getDialogHostComposeView(dialogShower as AlertDialogShower)
|
||||
)
|
||||
searchViewModel.setAlertDialogShower(dialogShower as AlertDialogShower)
|
||||
observeViewModelData()
|
||||
handleSearchArgument()
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
private fun handleSearchArgument() {
|
||||
val searchStringFromArguments = arguments?.getString(NAV_ARG_SEARCH_STRING)
|
||||
if (searchStringFromArguments != null) {
|
||||
onSearchValueChanged(searchStringFromArguments)
|
||||
}
|
||||
val argsCopy = Bundle(arguments)
|
||||
searchViewModel.actions.trySend(Action.CreatedWithArguments(argsCopy)).isSuccess
|
||||
arguments?.remove(EXTRA_IS_WIDGET_VOICE)
|
||||
}
|
||||
|
||||
private fun observeViewModelData() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
searchViewModel.effects.collect { effect ->
|
||||
effect.invokeWith(this@SearchFragment.coreMainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
searchViewModel.state.collect { state ->
|
||||
render(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
searchViewModel.voiceSearchResult.observe(viewLifecycleOwner) { searchTerm ->
|
||||
searchTerm?.let {
|
||||
onSearchValueChanged(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads more search results and appends them to the existing search results list in the RecyclerView.
|
||||
* This function is typically triggered when the RecyclerView is near about its last item.
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private fun loadMoreSearchResult() {
|
||||
if (isDataLoading) return
|
||||
isDataLoading = true
|
||||
val safeStartIndex = searchAdapter?.itemCount ?: 0
|
||||
if (isDataLoading.value) return
|
||||
isDataLoading.value = true
|
||||
val searchList = searchScreenState.value.searchList
|
||||
// Show a loading indicator while data is being loaded
|
||||
fragmentSearchBinding?.loadingMoreDataIndicator?.isShowing(true)
|
||||
searchScreenState.update { copy(shouldShowLoadingMoreProgressBar = true) }
|
||||
lifecycleScope.launch {
|
||||
// Request more search results from the ViewModel, providing the start
|
||||
// index and existing results
|
||||
searchViewModel.loadMoreSearchResults(safeStartIndex, searchAdapter?.items)
|
||||
searchViewModel.loadMoreSearchResults(searchList.size, searchList)
|
||||
.let { searchResults ->
|
||||
// Hide the loading indicator when data loading is complete
|
||||
fragmentSearchBinding?.loadingMoreDataIndicator?.isShowing(false)
|
||||
searchScreenState.update { copy(shouldShowLoadingMoreProgressBar = false) }
|
||||
// Update data loading status based on the received search results
|
||||
isDataLoading = when {
|
||||
isDataLoading.value = when {
|
||||
searchResults == null -> true
|
||||
searchResults.isEmpty() -> false
|
||||
else -> {
|
||||
// Append the new search results to the existing list
|
||||
searchAdapter?.addData(searchResults)
|
||||
searchScreenState.update {
|
||||
copy(searchList = searchScreenState.value.searchList + searchResults)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -203,34 +215,13 @@ class SearchFragment : BaseFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnnecessarySafeCall")
|
||||
private fun setupToolbar(view: View) {
|
||||
view.post {
|
||||
with(activity as? CoreMainActivity) {
|
||||
this?.setSupportActionBar(view.findViewById(R.id.toolbar))
|
||||
this?.supportActionBar?.apply {
|
||||
setHomeButtonEnabled(true)
|
||||
title = getString(R.string.menu_search_in_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
renderingJob?.cancel()
|
||||
renderingJob = null
|
||||
activity?.intent?.action = null
|
||||
searchView?.setOnQueryTextListener(null)
|
||||
searchView = null
|
||||
searchInTextMenuItem = null
|
||||
findInPageTextView = null
|
||||
searchMenuItem?.setOnActionExpandListener(null)
|
||||
searchMenuItem = null
|
||||
fragmentSearchBinding?.searchList?.adapter = null
|
||||
searchAdapter = null
|
||||
fragmentSearchBinding?.root?.removeAllViews()
|
||||
fragmentSearchBinding = null
|
||||
composeView?.disposeComposition()
|
||||
composeView = null
|
||||
}
|
||||
|
||||
private fun goBack() {
|
||||
@ -238,84 +229,48 @@ class SearchFragment : BaseFragment() {
|
||||
findNavController().popBackStack(readerFragmentResId, false)
|
||||
}
|
||||
|
||||
private fun setupMenu() {
|
||||
(requireActivity() as MenuHost).addMenuProvider(
|
||||
object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.menu_search, menu)
|
||||
searchMenuItem = menu.findItem(R.id.menu_search)
|
||||
searchMenuItem?.expandActionView()
|
||||
searchView = searchMenuItem?.actionView as SearchView
|
||||
searchView?.apply {
|
||||
setUpSearchView(requireActivity())
|
||||
searchView?.setOnQueryTextListener(
|
||||
SimpleTextListener { query, isSubmit ->
|
||||
if (query.isNotEmpty()) {
|
||||
setIsPageSearchEnabled(true)
|
||||
when {
|
||||
isSubmit -> {
|
||||
// if user press the search/enter button on keyboard,
|
||||
// try to open the article if present
|
||||
getSearchListItemForQuery(query)?.let(::onItemClick)
|
||||
}
|
||||
|
||||
else -> searchViewModel.actions.trySend(Filter(query)).isSuccess
|
||||
}
|
||||
} else {
|
||||
setIsPageSearchEnabled(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
searchMenuItem?.setOnActionExpandListener(
|
||||
object : OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem) = false
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
searchViewModel.actions.trySend(ExitedSearch).isSuccess
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
searchInTextMenuItem = menu.findItem(R.id.menu_searchintext)
|
||||
findInPageTextView =
|
||||
searchInTextMenuItem?.actionView?.findViewById(R.id.find_in_page_text_view)
|
||||
searchInTextMenuItem?.actionView?.setOnClickListener {
|
||||
searchViewModel.actions.trySend(ClickedSearchInText).isSuccess
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
searchViewModel.state.collect { render(it) }
|
||||
}
|
||||
}
|
||||
val searchStringFromArguments = arguments?.getString(NAV_ARG_SEARCH_STRING)
|
||||
if (searchStringFromArguments != null) {
|
||||
searchView?.setQuery(searchStringFromArguments, false)
|
||||
}
|
||||
searchViewModel.actions.trySend(Action.CreatedWithArguments(arguments)).isSuccess
|
||||
arguments?.remove(EXTRA_IS_WIDGET_VOICE)
|
||||
searchViewModel.voiceSearchResult.observe(viewLifecycleOwner) { searchTerm ->
|
||||
searchTerm?.let {
|
||||
searchView?.setQuery(it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem) = true
|
||||
},
|
||||
viewLifecycleOwner,
|
||||
Lifecycle.State.RESUMED
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSearchListItemForQuery(query: String): SearchListItem? =
|
||||
searchAdapter?.items?.firstOrNull {
|
||||
searchScreenState.value.searchList.firstOrNull {
|
||||
it.value.equals(query, ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun onSearchClear() {
|
||||
searchScreenState.update { copy(searchText = "") }
|
||||
setIsPageSearchEnabled("")
|
||||
searchEntryForSearchTerm("")
|
||||
}
|
||||
|
||||
private fun onSearchValueChanged(searchText: String) {
|
||||
searchScreenState.update { copy(searchText = searchText) }
|
||||
setIsPageSearchEnabled(searchText)
|
||||
searchEntryForSearchTerm(searchText)
|
||||
}
|
||||
|
||||
private fun searchEntryForSearchTerm(searchText: String) {
|
||||
searchViewModel.actions.trySend(Filter(searchText)).isSuccess
|
||||
}
|
||||
|
||||
private fun actionMenuItems() = listOfNotNull(
|
||||
// Check if the `FIND_IN_PAGE` is visible or not.
|
||||
// If visible then show it in menu.
|
||||
if (findInPageMenuItem.value.second) {
|
||||
ActionMenuItem(
|
||||
contentDescription = R.string.menu_search_in_text,
|
||||
onClick = {
|
||||
searchViewModel.actions.trySend(ClickedSearchInText).isSuccess
|
||||
},
|
||||
testingTag = FIND_IN_PAGE_TESTING_TAG,
|
||||
iconButtonText = R.string.menu_search_in_text,
|
||||
isEnabled = findInPageMenuItem.value.first
|
||||
)
|
||||
} else {
|
||||
// If `FIND_IN_PAGE` is not visible return null so that it will not show on the menu item.
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun render(state: SearchState) {
|
||||
private suspend fun render(state: SearchState) {
|
||||
renderingJob?.apply {
|
||||
// cancel the children job. Since we are getting the result on IO thread
|
||||
// with `withContext` that is child for this job
|
||||
@ -334,11 +289,10 @@ class SearchFragment : BaseFragment() {
|
||||
if (!isVisible) {
|
||||
return
|
||||
}
|
||||
isDataLoading = false
|
||||
searchInTextMenuItem?.actionView?.isVisible = state.searchOrigin == FromWebView
|
||||
setIsPageSearchEnabled(state.searchTerm.isNotBlank())
|
||||
|
||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(true)
|
||||
isDataLoading.value = false
|
||||
findInPageMenuItem.value = findInPageMenuItem.value.first to (state.searchOrigin == FromWebView)
|
||||
setIsPageSearchEnabled(state.searchTerm)
|
||||
searchScreenState.update { copy(isLoading = true) }
|
||||
renderingJob =
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
@ -347,10 +301,11 @@ class SearchFragment : BaseFragment() {
|
||||
state.getVisibleResults(0, coroutineContext[Job])
|
||||
}
|
||||
|
||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(false)
|
||||
searchScreenState.update { copy(isLoading = false) }
|
||||
searchResult?.let {
|
||||
fragmentSearchBinding?.searchNoResults?.isVisible = it.isEmpty()
|
||||
searchAdapter?.items = it
|
||||
searchScreenState.update {
|
||||
copy(searchList = it)
|
||||
}
|
||||
}
|
||||
} catch (ignore: CancellationException) {
|
||||
Log.e("SEARCH_RESULT", "Cancelled the previous job ${ignore.message}")
|
||||
@ -360,19 +315,13 @@ class SearchFragment : BaseFragment() {
|
||||
"Error in getting searched result\nOriginal exception ${ignore.message}"
|
||||
)
|
||||
} finally {
|
||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(false)
|
||||
searchScreenState.update { copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setIsPageSearchEnabled(isEnabled: Boolean) {
|
||||
searchInTextMenuItem?.actionView?.isEnabled = isEnabled
|
||||
findInPageTextView?.alpha =
|
||||
if (isEnabled) {
|
||||
ENABLED_SEARCH_IN_TEXT_OPACITY
|
||||
} else {
|
||||
DISABLED_SEARCH_IN_TEXT_OPACITY
|
||||
}
|
||||
private fun setIsPageSearchEnabled(searchText: String) {
|
||||
findInPageMenuItem.value = searchText.isNotBlank() to findInPageMenuItem.value.second
|
||||
}
|
||||
|
||||
private fun onItemClick(it: SearchListItem) {
|
||||
@ -388,14 +337,12 @@ class SearchFragment : BaseFragment() {
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
searchViewModel.actions.trySend(ActivityResultReceived(requestCode, resultCode, data)).isSuccess
|
||||
}
|
||||
}
|
||||
|
||||
private fun ContentLoadingProgressBar.isShowing(show: Boolean) {
|
||||
if (show) {
|
||||
show()
|
||||
} else {
|
||||
hide()
|
||||
searchViewModel.actions.trySend(
|
||||
ActivityResultReceived(
|
||||
requestCode,
|
||||
resultCode,
|
||||
data
|
||||
)
|
||||
).isSuccess
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
* Copyright (c) 2025 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
@ -16,7 +16,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.search.adapter
|
||||
package org.kiwix.kiwixmobile.core.search
|
||||
|
||||
sealed class SearchListItem {
|
||||
abstract val value: String
|
@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2025 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.search
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
|
||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
|
||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView
|
||||
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.LOAD_MORE_PROGRESS_BAR_SIZE
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.MINIMUM_HEIGHT_OF_SEARCH_ITEM
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SEARCH_ITEM_TEXT_SIZE
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.THREE_DP
|
||||
|
||||
const val SEARCH_FIELD_TESTING_TAG = "searchFieldTestingTag"
|
||||
const val NO_SEARCH_RESULT_TESTING_TAG = "noSearchResultTestingTag"
|
||||
const val FIND_IN_PAGE_TESTING_TAG = "findInPageTestingTag"
|
||||
const val SEARCH_ITEM_TESTING_TAG = "searchItemTestingTag"
|
||||
const val LOADING_ITEMS_BEFORE = 3
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
searchScreenState: SearchScreenState,
|
||||
actionMenuItemList: List<ActionMenuItem>,
|
||||
isLoadingMoreResult: Boolean
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
KiwixTheme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
KiwixAppBar(
|
||||
titleId = R.string.empty_string,
|
||||
navigationIcon = searchScreenState.navigationIcon,
|
||||
actionMenuItems = actionMenuItemList,
|
||||
searchBar = {
|
||||
KiwixSearchView(
|
||||
value = searchScreenState.searchText,
|
||||
searchViewTextFiledTestTag = SEARCH_FIELD_TESTING_TAG,
|
||||
onValueChange = searchScreenState.onSearchViewValueChange,
|
||||
onClearClick = searchScreenState.onSearchViewClearClick,
|
||||
modifier = Modifier,
|
||||
onKeyboardSubmitButtonClick = searchScreenState.onKeyboardSubmitButtonClick
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
SearchScreenContent(searchScreenState, innerPadding, lazyListState)
|
||||
}
|
||||
}
|
||||
InfiniteListHandler(
|
||||
listState = lazyListState,
|
||||
isLoadingMoreResult = isLoadingMoreResult,
|
||||
onLoadMore = searchScreenState.onLoadMore
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchScreenContent(
|
||||
searchScreenState: SearchScreenState,
|
||||
innerPadding: PaddingValues,
|
||||
lazyListState: LazyListState
|
||||
) {
|
||||
val progressBarTrackColor = MaterialTheme.colorScheme.background
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = innerPadding.calculateTopPadding(),
|
||||
start = innerPadding.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = innerPadding.calculateEndPadding(LocalLayoutDirection.current)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (searchScreenState.searchList.isEmpty()) {
|
||||
NoSearchResultView()
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
state = lazyListState
|
||||
) {
|
||||
items(searchScreenState.searchList) { item ->
|
||||
SearchListItem(
|
||||
searchListItem = item,
|
||||
onItemClick = { searchScreenState.onItemClick(item) },
|
||||
onNewTabIconClick = { searchScreenState.onNewTabIconClick(item) },
|
||||
onItemLongClick = if (item is SearchListItem.RecentSearchListItem) {
|
||||
{ searchScreenState.onItemLongClick(item) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
showLoadMoreProgressBar(searchScreenState, progressBarTrackColor)
|
||||
}
|
||||
}
|
||||
ShowLoadingProgressBar(searchScreenState.isLoading, progressBarTrackColor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.showLoadMoreProgressBar(
|
||||
searchScreenState: SearchScreenState,
|
||||
progressBarTrackColor: Color
|
||||
) {
|
||||
if (searchScreenState.shouldShowLoadingMoreProgressBar) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(SIXTEEN_DP),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ContentLoadingProgressBar(
|
||||
modifier = Modifier.size(LOAD_MORE_PROGRESS_BAR_SIZE),
|
||||
circularProgressBarStockWidth = THREE_DP,
|
||||
progressBarTrackColor = progressBarTrackColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowLoadingProgressBar(isLoading: Boolean, progressBarTrackColor: Color) {
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ContentLoadingProgressBar(progressBarTrackColor = progressBarTrackColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoSearchResultView() {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = FOUR_DP)
|
||||
.semantics { testTag = NO_SEARCH_RESULT_TESTING_TAG }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun SearchListItem(
|
||||
searchListItem: SearchListItem,
|
||||
onNewTabIconClick: (SearchListItem) -> Unit,
|
||||
onItemClick: (SearchListItem) -> Unit,
|
||||
onItemLongClick: ((SearchListItem) -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = MINIMUM_HEIGHT_OF_SEARCH_ITEM),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = searchListItem.value,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = EIGHT_DP)
|
||||
.combinedClickable(
|
||||
onClick = { onItemClick(searchListItem) },
|
||||
onLongClick = { onItemLongClick?.invoke(searchListItem) }
|
||||
)
|
||||
.semantics { testTag = SEARCH_ITEM_TESTING_TAG },
|
||||
fontSize = SEARCH_ITEM_TEXT_SIZE,
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { onNewTabIconClick(searchListItem) },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_open_in_new_24dp),
|
||||
contentDescription = stringResource(id = R.string.search_open_in_new_tab),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfiniteListHandler(
|
||||
listState: LazyListState,
|
||||
buffer: Int = LOADING_ITEMS_BEFORE,
|
||||
isLoadingMoreResult: Boolean,
|
||||
onLoadMore: () -> Unit
|
||||
) {
|
||||
val shouldLoadMore = remember {
|
||||
derivedStateOf {
|
||||
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
val totalItemCount = listState.layoutInfo.totalItemsCount
|
||||
!isLoadingMoreResult && lastVisibleItemIndex >= totalItemCount - buffer
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
snapshotFlow { shouldLoadMore.value }.collect { load ->
|
||||
if (load) onLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2025 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.search
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
data class SearchScreenState(
|
||||
/**
|
||||
* Manages the search screen list state.
|
||||
*/
|
||||
val searchList: List<SearchListItem>,
|
||||
/**
|
||||
* Manages the showing of loading progress at the initial.
|
||||
*/
|
||||
val isLoading: Boolean,
|
||||
/**
|
||||
* Manages the showing of load more progress at the end of search list.
|
||||
*/
|
||||
val shouldShowLoadingMoreProgressBar: Boolean,
|
||||
/**
|
||||
* Handles the calling for more items.
|
||||
*/
|
||||
val onLoadMore: () -> Unit,
|
||||
/**
|
||||
* Stores the searchView text, and displayed it inside the searchView.
|
||||
*/
|
||||
val searchText: String,
|
||||
/**
|
||||
* Handles the click on searchView's close button.
|
||||
*/
|
||||
val onSearchViewClearClick: () -> Unit,
|
||||
/**
|
||||
* Handles the changing of searchView values.
|
||||
*/
|
||||
val onSearchViewValueChange: (String) -> Unit,
|
||||
/**
|
||||
* Handles the item click on searchItem
|
||||
*/
|
||||
val onItemClick: (SearchListItem) -> Unit,
|
||||
/**
|
||||
* Handles the long click on searchItem.
|
||||
*/
|
||||
val onItemLongClick: (SearchListItem) -> Unit,
|
||||
/**
|
||||
* Handles the newTabIcon click.
|
||||
*/
|
||||
val onNewTabIconClick: (SearchListItem) -> Unit,
|
||||
/**
|
||||
* Handles the Keyboard submit button click.
|
||||
*/
|
||||
val onKeyboardSubmitButtonClick: (String) -> Unit,
|
||||
/**
|
||||
* Manages the navigationIcon shown in the toolbar.
|
||||
*/
|
||||
val navigationIcon: @Composable() () -> Unit
|
||||
)
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.search.adapter
|
||||
|
||||
import org.kiwix.kiwixmobile.core.base.adapter.AdapterDelegate
|
||||
import org.kiwix.kiwixmobile.core.base.adapter.BaseDelegateAdapter
|
||||
|
||||
class SearchAdapter(
|
||||
vararg delegates: AdapterDelegate<SearchListItem>
|
||||
) : BaseDelegateAdapter<SearchListItem>(*delegates) {
|
||||
override fun getIdFor(item: SearchListItem) = item.value.hashCode().toLong()
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.search.adapter
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.kiwix.kiwixmobile.core.base.adapter.AbsDelegateAdapter
|
||||
import org.kiwix.kiwixmobile.core.databinding.ListItemSearchBinding
|
||||
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.viewBinding
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchViewHolder.RecentSearchViewHolder
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchViewHolder.ZimSearchResultViewHolder
|
||||
|
||||
sealed class SearchDelegate<I : SearchListItem, out VH : SearchViewHolder<I>> :
|
||||
AbsDelegateAdapter<I, SearchListItem, VH> {
|
||||
class RecentSearchDelegate(
|
||||
private val onClickListener: (SearchListItem) -> Unit,
|
||||
private val onClickListenerNewTab: (SearchListItem) -> Unit,
|
||||
private val onLongClickListener: (SearchListItem) -> Unit
|
||||
) : SearchDelegate<RecentSearchListItem, RecentSearchViewHolder>() {
|
||||
override val itemClass = RecentSearchListItem::class.java
|
||||
|
||||
override fun createViewHolder(parent: ViewGroup) =
|
||||
RecentSearchViewHolder(
|
||||
parent.viewBinding(ListItemSearchBinding::inflate, false),
|
||||
onClickListener,
|
||||
onClickListenerNewTab,
|
||||
onLongClickListener
|
||||
)
|
||||
}
|
||||
|
||||
class ZimSearchResultDelegate(
|
||||
private val onClickListener: (SearchListItem) -> Unit,
|
||||
private val onClickListenerNewTab: (SearchListItem) -> Unit
|
||||
) : SearchDelegate<ZimSearchResultListItem, ZimSearchResultViewHolder>() {
|
||||
override val itemClass = ZimSearchResultListItem::class.java
|
||||
|
||||
override fun createViewHolder(parent: ViewGroup) =
|
||||
ZimSearchResultViewHolder(
|
||||
parent.viewBinding(ListItemSearchBinding::inflate, false),
|
||||
onClickListener,
|
||||
onClickListenerNewTab
|
||||
)
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.search.adapter
|
||||
|
||||
import android.view.View
|
||||
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
|
||||
import org.kiwix.kiwixmobile.core.databinding.ListItemSearchBinding
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem
|
||||
|
||||
sealed class SearchViewHolder<in T : SearchListItem>(containerView: View) :
|
||||
BaseViewHolder<T>(containerView) {
|
||||
class RecentSearchViewHolder(
|
||||
private val listItemSearchBinding: ListItemSearchBinding,
|
||||
private val onClickListener: (SearchListItem) -> Unit,
|
||||
private val onClickListenerNewTab: (SearchListItem) -> Unit,
|
||||
private val onLongClickListener: (SearchListItem) -> Unit
|
||||
) : SearchViewHolder<RecentSearchListItem>(listItemSearchBinding.root) {
|
||||
override fun bind(item: RecentSearchListItem) {
|
||||
containerView.setOnClickListener { onClickListener(item) }
|
||||
containerView.setOnLongClickListener {
|
||||
onLongClickListener(item)
|
||||
true
|
||||
}
|
||||
listItemSearchBinding.listItemSearchNewTabButton.setOnClickListener {
|
||||
onClickListenerNewTab(
|
||||
item
|
||||
)
|
||||
}
|
||||
listItemSearchBinding.listItemSearchText.text = item.value
|
||||
}
|
||||
}
|
||||
|
||||
class ZimSearchResultViewHolder(
|
||||
private val listItemSearchBinding: ListItemSearchBinding,
|
||||
private val onClickListener: (SearchListItem) -> Unit,
|
||||
private val onClickListenerNewTab: (SearchListItem) -> Unit
|
||||
) : SearchViewHolder<ZimSearchResultListItem>(listItemSearchBinding.root) {
|
||||
override fun bind(item: ZimSearchResultListItem) {
|
||||
containerView.setOnClickListener { onClickListener(item) }
|
||||
listItemSearchBinding.listItemSearchNewTabButton.setOnClickListener {
|
||||
onClickListenerNewTab(
|
||||
item
|
||||
)
|
||||
}
|
||||
listItemSearchBinding.listItemSearchText.text = item.value
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ package org.kiwix.kiwixmobile.core.search.viewmodel
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
|
||||
sealed class Action {
|
||||
object ExitedSearch : Action()
|
||||
|
@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.core.search.viewmodel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.yield
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||
|
||||
data class SearchState(
|
||||
|
@ -36,7 +36,7 @@ import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ActivityResultReceived
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
||||
@ -84,7 +84,7 @@ class SearchViewModel @Inject constructor(
|
||||
FromWebView
|
||||
)
|
||||
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
||||
private val _effects = Channel<SideEffect<*>>()
|
||||
private val _effects = Channel<SideEffect<*>>(Channel.UNLIMITED)
|
||||
val effects = _effects.receiveAsFlow()
|
||||
val actions = Channel<Action>(Channel.UNLIMITED)
|
||||
private val filter = MutableStateFlow("")
|
||||
|
@ -24,7 +24,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
data class DeleteRecentSearch(
|
||||
|
@ -27,7 +27,7 @@ import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationRes
|
||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.main.SEARCH_ITEM_TITLE_KEY
|
||||
import org.kiwix.kiwixmobile.core.reader.addContentPrefix
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED
|
||||
|
||||
data class OpenSearchItem(
|
||||
|
@ -25,7 +25,7 @@ import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||
import org.kiwix.kiwixmobile.core.reader.addContentPrefix
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
data class SaveSearchToRecents(
|
||||
|
@ -22,7 +22,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSearch
|
||||
|
@ -46,7 +46,7 @@ data class StartSpeechInput(private val actions: Channel<Action>) : SideEffect<U
|
||||
},
|
||||
REQ_CODE_SPEECH_INPUT
|
||||
)
|
||||
} catch (a: ActivityNotFoundException) {
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
actions.trySend(StartSpeechInputFailed).isSuccess
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
@ -135,16 +136,34 @@ private fun AppBarTitle(
|
||||
private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
|
||||
Row {
|
||||
actionMenuItems.forEach { menuItem ->
|
||||
IconButton(
|
||||
enabled = menuItem.isEnabled,
|
||||
onClick = menuItem.onClick,
|
||||
modifier = menuItem.modifier.testTag(menuItem.testingTag)
|
||||
) {
|
||||
Icon(
|
||||
painter = menuItem.icon.toPainter(),
|
||||
contentDescription = stringResource(menuItem.contentDescription),
|
||||
tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray
|
||||
)
|
||||
val modifier = menuItem.modifier.testTag(menuItem.testingTag)
|
||||
// If icon is not null show the icon.
|
||||
menuItem.icon?.let {
|
||||
IconButton(
|
||||
enabled = menuItem.isEnabled,
|
||||
onClick = menuItem.onClick,
|
||||
modifier = modifier
|
||||
) {
|
||||
Icon(
|
||||
painter = it.toPainter(),
|
||||
contentDescription = stringResource(menuItem.contentDescription),
|
||||
tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
// Else show the textView button in menuItem.
|
||||
TextButton(
|
||||
enabled = menuItem.isEnabled,
|
||||
onClick = menuItem.onClick,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = menuItem.iconButtonText).uppercase(),
|
||||
color = if (menuItem.isEnabled) Color.White else Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.ui.components
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
@ -30,10 +32,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
|
||||
|
||||
@ -45,8 +49,10 @@ fun KiwixSearchView(
|
||||
searchViewTextFiledTestTag: String = "",
|
||||
clearButtonTestTag: String = "",
|
||||
onValueChange: (String) -> Unit,
|
||||
onClearClick: () -> Unit
|
||||
onClearClick: () -> Unit,
|
||||
onKeyboardSubmitButtonClick: (String) -> Unit = {}
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
@ -89,6 +95,15 @@ fun KiwixSearchView(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
keyboardController?.hide()
|
||||
onKeyboardSubmitButtonClick.invoke(value)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -24,13 +24,18 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
||||
import org.kiwix.kiwixmobile.core.ui.models.toPainter
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||
|
||||
const val NAVIGATION_ICON_TESTING_TAG = "navigationIconTestingTag"
|
||||
|
||||
/**
|
||||
* A composable function that renders a navigation icon, which can be either a vector
|
||||
* or drawable image.
|
||||
@ -47,9 +52,10 @@ fun NavigationIcon(
|
||||
iconItem: IconItem = IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack),
|
||||
onClick: () -> Unit,
|
||||
@StringRes contentDescription: Int = R.string.toolbar_back_button_content_description,
|
||||
iconTint: Color = White
|
||||
iconTint: Color = White,
|
||||
testingTag: String = NAVIGATION_ICON_TESTING_TAG
|
||||
) {
|
||||
IconButton(onClick = onClick) {
|
||||
IconButton(onClick = onClick, modifier = Modifier.semantics { testTag = testingTag }) {
|
||||
Icon(
|
||||
painter = iconItem.toPainter(),
|
||||
contentDescription = stringResource(contentDescription),
|
||||
|
@ -21,14 +21,16 @@ package org.kiwix.kiwixmobile.core.ui.models
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||
|
||||
data class ActionMenuItem(
|
||||
val icon: IconItem,
|
||||
val icon: IconItem? = null,
|
||||
@StringRes val contentDescription: Int,
|
||||
val onClick: () -> Unit,
|
||||
val iconTint: Color = White,
|
||||
val isEnabled: Boolean = true,
|
||||
@StringRes val iconButtonText: Int = R.string.empty_string,
|
||||
val testingTag: String,
|
||||
val modifier: Modifier = Modifier
|
||||
)
|
||||
|
@ -35,8 +35,8 @@ object ComposeDimens {
|
||||
// Error screen dimens
|
||||
val CRASH_IMAGE_SIZE = 70.dp
|
||||
|
||||
// KiwixAppBar(Toolbar) height
|
||||
val KIWIX_APP_BAR_HEIGHT = 56.dp
|
||||
// KiwixAppBar(Toolbar) dimens
|
||||
val ACTION_MENU_TEXTVIEW_BUTTON_PADDING = 13.dp
|
||||
|
||||
// Padding & Margins
|
||||
val SIXTY_DP = 60.dp
|
||||
@ -51,6 +51,7 @@ object ComposeDimens {
|
||||
val SIX_DP = 6.dp
|
||||
val FIVE_DP = 5.dp
|
||||
val FOUR_DP = 4.dp
|
||||
val THREE_DP = 3.dp
|
||||
val TWO_DP = 2.dp
|
||||
val ONE_DP = 1.dp
|
||||
val SEVENTY_DP = 70.dp
|
||||
@ -169,4 +170,9 @@ object ComposeDimens {
|
||||
val DOWNLOADING_LIBRARY_MESSAGE_TEXT_SIZE = 8.sp
|
||||
val DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE = 30.dp
|
||||
const val ONLINE_BOOK_DISABLED_COLOR_ALPHA = 0.5F
|
||||
|
||||
// Search screen dimens
|
||||
val MINIMUM_HEIGHT_OF_SEARCH_ITEM = 64.dp
|
||||
val SEARCH_ITEM_TEXT_SIZE = 16.sp
|
||||
val LOAD_MORE_PROGRESS_BAR_SIZE = 40.dp
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2021 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.utils
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class SimpleRecyclerViewScrollListener(
|
||||
private val onLayoutScrollListener: (RecyclerView, Int) -> Unit
|
||||
) :
|
||||
RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
onLayoutScrollListener(
|
||||
recyclerView,
|
||||
newState
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
val currentScrollPosition = recyclerView.computeVerticalScrollOffset()
|
||||
|
||||
if (currentScrollPosition > previousScrollPosition) {
|
||||
onLayoutScrollListener(
|
||||
recyclerView,
|
||||
SCROLL_DOWN
|
||||
)
|
||||
} else if (currentScrollPosition < previousScrollPosition) {
|
||||
onLayoutScrollListener(
|
||||
recyclerView,
|
||||
SCROLL_UP
|
||||
)
|
||||
}
|
||||
|
||||
previousScrollPosition = currentScrollPosition
|
||||
}
|
||||
|
||||
private var previousScrollPosition = 0
|
||||
|
||||
companion object {
|
||||
const val SCROLL_DOWN = 2000
|
||||
const val SCROLL_UP = 2001
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.utils
|
||||
|
||||
import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||
|
||||
class SimpleTextListener(private val onQueryTextChangeAction: (String, Boolean) -> Unit) :
|
||||
OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(s: String): Boolean {
|
||||
onQueryTextChangeAction.invoke(s, true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(s: String): Boolean {
|
||||
onQueryTextChangeAction.invoke(s, false)
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2019 Kiwix <android.kiwix.org>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.kiwix.kiwixmobile.core.zim_manager
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import org.kiwix.kiwixmobile.core.databinding.TagContentBinding
|
||||
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag
|
||||
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag.DetailsTag
|
||||
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag.PicturesTag
|
||||
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag.VideoTag
|
||||
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.TagValue.YES
|
||||
|
||||
class TagsView(context: Context, attrs: AttributeSet) : ChipGroup(context, attrs) {
|
||||
private var tagContentBinding: TagContentBinding? = null
|
||||
|
||||
init {
|
||||
tagContentBinding = TagContentBinding.inflate(LayoutInflater.from(context), this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
tagContentBinding = null
|
||||
}
|
||||
|
||||
fun render(tags: List<KiwixTag>) {
|
||||
tagContentBinding?.tagPicture?.selectBy(tags.isYesOrNotDefined<PicturesTag>())
|
||||
tagContentBinding?.tagVideo?.selectBy(tags.isYesOrNotDefined<VideoTag>())
|
||||
val shortTextIsSelected = tags.isDefinedAndNo<DetailsTag>()
|
||||
tagContentBinding?.tagTextOnly?.selectBy(
|
||||
tags.isDefinedAndNo<PicturesTag>() &&
|
||||
tags.isDefinedAndNo<VideoTag>() &&
|
||||
!shortTextIsSelected
|
||||
)
|
||||
tagContentBinding?.tagShortText?.selectBy(shortTextIsSelected)
|
||||
}
|
||||
|
||||
private inline fun <reified T : YesNoValueTag> List<KiwixTag>.isYesOrNotDefined() =
|
||||
isYes<T>() || !isDefined<T>()
|
||||
|
||||
private inline fun <reified T : YesNoValueTag> List<KiwixTag>.isDefinedAndNo() =
|
||||
isDefined<T>() && !isYes<T>()
|
||||
|
||||
private inline fun <reified T : YesNoValueTag> List<KiwixTag>.isYes() =
|
||||
filterIsInstance<T>().getOrNull(0)?.value == YES
|
||||
|
||||
private inline fun <reified T : YesNoValueTag> List<KiwixTag>.isDefined() =
|
||||
filterIsInstance<T>().isNotEmpty()
|
||||
|
||||
private fun Chip.selectBy(criteria: Boolean) {
|
||||
isChecked = criteria
|
||||
isEnabled = criteria
|
||||
visibility = if (criteria) VISIBLE else GONE
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Kiwix Android
|
||||
~ Copyright (c) 2024 Kiwix <android.kiwix.org>
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
~
|
||||
-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/find_in_page_text_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:padding="@dimen/find_in_page_button_padding"
|
||||
android:text="@string/menu_search_in_text"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:textColor="@android:color/black" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,56 +0,0 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/layout_toolbar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/search_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="?actionBarSize"
|
||||
android:clipToPadding="false"
|
||||
android:contentDescription="@string/searched_list"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loadingMoreDataIndicator"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:listitem="@layout/list_item_search" />
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/loadingMoreDataIndicator"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/searchNoResults"
|
||||
style="@style/no_content"
|
||||
android:text="@string/no_results" />
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/searchLoadingIndicator"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -7,8 +7,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:popupTheme="@style/KiwixTheme"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||
tools:showIn="@layout/fragment_search">
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/toolbarWithSearchPlaceholder"
|
||||
|
@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Kiwix Android
|
||||
~ Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
~
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/list_item_search_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/listChoiceBackgroundIndicator"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:paddingStart="8dip"
|
||||
android:paddingEnd="8dip"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
app:layout_constraintEnd_toStartOf="@+id/list_item_search_new_tab_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/list_item_search_new_tab_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/search_open_in_new_tab"
|
||||
android:src="@drawable/ic_open_in_new_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="com.google.android.material.chip.ChipGroup">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/tag_picture"
|
||||
style="@style/Widget.KiwixTheme.Chip.Choice.Static"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tag_pic" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/tag_video"
|
||||
style="@style/Widget.KiwixTheme.Chip.Choice.Static"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tag_vid" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/tag_short_text"
|
||||
style="@style/Widget.KiwixTheme.Chip.Choice.Static"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tag_short_text" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/tag_text_only"
|
||||
style="@style/Widget.KiwixTheme.Chip.Choice.Static"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tag_text_only" />
|
||||
</merge>
|
@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Kiwix Android
|
||||
~ Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
~
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_page_search"
|
||||
android:icon="@drawable/action_search"
|
||||
android:title="@string/search_label"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:iconifiedByDefault="true"
|
||||
app:showAsAction="always|collapseActionView"
|
||||
tools:ignore="AlwaysShowAction" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_pages_clear"
|
||||
android:icon="@drawable/ic_delete_white_24dp"
|
||||
android:title="@string/pref_clear_all_bookmarks_title"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_search"
|
||||
android:icon="@drawable/action_search"
|
||||
android:title="@string/search_label"
|
||||
android:visible="false"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:iconifiedByDefault="false"
|
||||
app:showAsAction="ifRoom|collapseActionView" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_searchintext"
|
||||
android:title="@string/menu_search_in_text"
|
||||
app:actionLayout = "@layout/action_searchintext"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
@ -30,7 +30,7 @@ import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity_
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.test
|
||||
import org.kiwix.sharedFunctions.recentSearchEntity
|
||||
|
||||
|
@ -27,8 +27,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView
|
||||
|
||||
internal class SearchStateTest {
|
||||
@ -123,8 +122,8 @@ internal class SearchStateTest {
|
||||
val searchResultsWithTerm =
|
||||
SearchResultsWithTerm(searchTerm, suggestionSearchWrapper, mockk())
|
||||
val searchState = SearchState(searchTerm, searchResultsWithTerm, emptyList(), FromWebView)
|
||||
var list: List<SearchListItem.RecentSearchListItem>? = emptyList()
|
||||
var list1: List<SearchListItem.RecentSearchListItem>? = emptyList()
|
||||
var list: List<RecentSearchListItem>? = emptyList()
|
||||
var list1: List<RecentSearchListItem>? = emptyList()
|
||||
val job =
|
||||
launch(Dispatchers.IO) {
|
||||
delay(1000)
|
||||
|
@ -50,7 +50,7 @@ import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ActivityResultReceived
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
||||
|
@ -27,8 +27,8 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
|
||||
internal class DeleteRecentSearchTest {
|
||||
@Test
|
||||
|
@ -28,7 +28,7 @@ import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationResultOnCurrent
|
||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED
|
||||
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED_NEW_TAB
|
||||
|
||||
|
@ -29,7 +29,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
|
||||
internal class SaveSearchToRecentsTest {
|
||||
private val newRecentSearchDao: RecentSearchRoomDao = mockk()
|
||||
|
@ -25,7 +25,7 @@ import io.mockk.verify
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
||||
|
@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.custom.search
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.res.AssetFileDescriptor
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
@ -52,6 +53,8 @@ import org.kiwix.kiwixmobile.core.search.SearchFragment
|
||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||
import org.kiwix.kiwixmobile.core.utils.LanguageUtils
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER
|
||||
import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER
|
||||
import org.kiwix.kiwixmobile.custom.main.CustomMainActivity
|
||||
import org.kiwix.kiwixmobile.custom.main.CustomReaderFragment
|
||||
import org.kiwix.kiwixmobile.custom.testutils.RetryRule
|
||||
@ -80,10 +83,13 @@ class SearchFragmentTestForCustomApp {
|
||||
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||
}
|
||||
|
||||
@Rule
|
||||
@Rule(order = RETRY_RULE_ORDER)
|
||||
@JvmField
|
||||
var retryRule = RetryRule()
|
||||
|
||||
@get:Rule(order = COMPOSE_TEST_RULE_ORDER)
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private lateinit var customMainActivity: CustomMainActivity
|
||||
private lateinit var uiDevice: UiDevice
|
||||
private lateinit var downloadingZimFile: File
|
||||
@ -151,24 +157,24 @@ class SearchFragmentTestForCustomApp {
|
||||
openZimFileInReader(zimFile = downloadingZimFile)
|
||||
openSearchWithQuery()
|
||||
val searchTerm = "gard"
|
||||
val searchedItem = "Gardanta Spirito"
|
||||
val searchedItem = "Gardanta Spirito - Андивионский Научный Альянс"
|
||||
search {
|
||||
// test with fast typing/deleting
|
||||
searchWithFrequentlyTypedWords(searchTerm)
|
||||
assertSearchSuccessful(searchedItem)
|
||||
deleteSearchedQueryFrequently(searchTerm, uiDevice)
|
||||
searchWithFrequentlyTypedWords(searchTerm, composeTestRule = composeTestRule)
|
||||
assertSearchSuccessful(searchedItem, composeTestRule)
|
||||
deleteSearchedQueryFrequently(searchTerm, uiDevice, composeTestRule = composeTestRule)
|
||||
|
||||
// test with a short delay typing/deleting to
|
||||
// properly test the cancelling of previously searching task
|
||||
searchWithFrequentlyTypedWords(searchTerm, 50)
|
||||
assertSearchSuccessful(searchedItem)
|
||||
deleteSearchedQueryFrequently(searchTerm, uiDevice, 50)
|
||||
searchWithFrequentlyTypedWords(searchTerm, 50, composeTestRule)
|
||||
assertSearchSuccessful(searchedItem, composeTestRule)
|
||||
deleteSearchedQueryFrequently(searchTerm, uiDevice, 50, composeTestRule)
|
||||
|
||||
// test with a long delay typing/deleting to
|
||||
// properly execute the search query letter by letter
|
||||
searchWithFrequentlyTypedWords(searchTerm, 300)
|
||||
assertSearchSuccessful(searchedItem)
|
||||
deleteSearchedQueryFrequently(searchTerm, uiDevice, 300)
|
||||
searchWithFrequentlyTypedWords(searchTerm, 300, composeTestRule)
|
||||
assertSearchSuccessful(searchedItem, composeTestRule)
|
||||
deleteSearchedQueryFrequently(searchTerm, uiDevice, 300, composeTestRule)
|
||||
// to close the keyboard
|
||||
pressBack()
|
||||
// go to reader screen
|
||||
@ -179,16 +185,16 @@ class SearchFragmentTestForCustomApp {
|
||||
// frequently searched for article, and clicked on the searched item.
|
||||
search {
|
||||
// test by searching 10 article and clicking on them
|
||||
searchAndClickOnArticle(searchTerm)
|
||||
searchAndClickOnArticle("eilum")
|
||||
searchAndClickOnArticle("page")
|
||||
searchAndClickOnArticle("list")
|
||||
searchAndClickOnArticle("ladder")
|
||||
searchAndClickOnArticle("welc")
|
||||
searchAndClickOnArticle("js")
|
||||
searchAndClickOnArticle("hizo")
|
||||
searchAndClickOnArticle("fad")
|
||||
searchAndClickOnArticle("forum")
|
||||
searchAndClickOnArticle(searchTerm, composeTestRule)
|
||||
searchAndClickOnArticle("eilum", composeTestRule)
|
||||
searchAndClickOnArticle("page", composeTestRule)
|
||||
searchAndClickOnArticle("list", composeTestRule)
|
||||
searchAndClickOnArticle("ladder", composeTestRule)
|
||||
searchAndClickOnArticle("welc", composeTestRule)
|
||||
searchAndClickOnArticle("js", composeTestRule)
|
||||
searchAndClickOnArticle("hizo", composeTestRule)
|
||||
searchAndClickOnArticle("fad", composeTestRule)
|
||||
searchAndClickOnArticle("forum", composeTestRule)
|
||||
assertArticleLoaded()
|
||||
}
|
||||
}
|
||||
|
@ -19,14 +19,19 @@
|
||||
package org.kiwix.kiwixmobile.custom.search
|
||||
|
||||
import android.view.KeyEvent
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
|
||||
@ -40,56 +45,68 @@ import androidx.test.espresso.web.webdriver.Locator
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawerWithGravity
|
||||
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
|
||||
import com.adevinta.android.barista.internal.matcher.HelperMatchers
|
||||
import org.hamcrest.CoreMatchers.containsString
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG
|
||||
import org.kiwix.kiwixmobile.custom.R.id
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.TEST_PAUSE_MS
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.testFlakyView
|
||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.waitUntilTimeout
|
||||
|
||||
fun search(searchRobot: SearchRobot.() -> Unit) = SearchRobot().searchRobot()
|
||||
|
||||
class SearchRobot {
|
||||
fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) {
|
||||
fun searchWithFrequentlyTypedWords(
|
||||
query: String,
|
||||
wait: Long = 0L,
|
||||
composeTestRule: ComposeContentTestRule
|
||||
) {
|
||||
testFlakyView({
|
||||
val searchView = Espresso.onView(ViewMatchers.withId(androidx.appcompat.R.id.search_src_text))
|
||||
searchView.perform(ViewActions.clearText())
|
||||
for (char in query) {
|
||||
searchView.perform(ViewActions.typeText(char.toString()))
|
||||
if (wait != 0L) {
|
||||
BaristaSleepInteractions.sleep(wait)
|
||||
composeTestRule.apply {
|
||||
waitUntilTimeout()
|
||||
val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG)
|
||||
searchView.performTextInput("")
|
||||
for (char in query) {
|
||||
searchView.performTextInput(char.toString())
|
||||
if (wait != 0L) {
|
||||
waitUntilTimeout(wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun assertSearchSuccessful(searchResult: String) {
|
||||
testFlakyView({
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.search_list)).check(
|
||||
ViewAssertions.matches(
|
||||
HelperMatchers.atPosition(
|
||||
0,
|
||||
ViewMatchers.hasDescendant(ViewMatchers.withSubstring(searchResult))
|
||||
)
|
||||
)
|
||||
fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) {
|
||||
composeTestRule.apply {
|
||||
waitUntil(
|
||||
timeoutMillis = TEST_PAUSE_MS.toLong(),
|
||||
condition = {
|
||||
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)
|
||||
.fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
)
|
||||
})
|
||||
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0]
|
||||
.assert(hasText(searchResult))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) {
|
||||
testFlakyView({
|
||||
for (i in textToDelete.indices) {
|
||||
uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL)
|
||||
if (wait != 0L) {
|
||||
BaristaSleepInteractions.sleep(wait)
|
||||
}
|
||||
fun deleteSearchedQueryFrequently(
|
||||
textToDelete: String,
|
||||
uiDevice: UiDevice,
|
||||
wait: Long = 0L,
|
||||
composeTestRule: ComposeContentTestRule
|
||||
) {
|
||||
repeat(textToDelete.length) {
|
||||
uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL)
|
||||
if (wait != 0L) {
|
||||
BaristaSleepInteractions.sleep(wait)
|
||||
}
|
||||
}
|
||||
|
||||
// clear search query if any remains due to any condition not to affect any other test scenario
|
||||
val searchView = Espresso.onView(ViewMatchers.withId(androidx.appcompat.R.id.search_src_text))
|
||||
searchView.perform(ViewActions.clearText())
|
||||
})
|
||||
// clear search query if any remains due to any condition not to affect any other test scenario
|
||||
composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance()
|
||||
}
|
||||
|
||||
private fun openSearchScreen() {
|
||||
@ -99,26 +116,21 @@ class SearchRobot {
|
||||
})
|
||||
}
|
||||
|
||||
fun searchAndClickOnArticle(searchString: String) {
|
||||
// Wait a bit to properly load the ZIM file in the reader.
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) {
|
||||
// wait a bit to properly load the ZIM file in the reader
|
||||
composeTestRule.waitUntilTimeout()
|
||||
openSearchScreen()
|
||||
// Wait a bit to properly visible the search screen.
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
searchWithFrequentlyTypedWords(searchString)
|
||||
clickOnSearchItemInSearchList()
|
||||
composeTestRule.waitUntilTimeout()
|
||||
searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule)
|
||||
clickOnSearchItemInSearchList(composeTestRule)
|
||||
}
|
||||
|
||||
private fun clickOnSearchItemInSearchList() {
|
||||
testFlakyView({
|
||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.search_list)).perform(
|
||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
||||
0,
|
||||
ViewActions.click()
|
||||
)
|
||||
)
|
||||
})
|
||||
private fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) {
|
||||
composeTestRule.apply {
|
||||
waitUntilTimeout()
|
||||
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun assertArticleLoaded() {
|
||||
|
@ -21,12 +21,15 @@ package org.kiwix.kiwixmobile.custom.testutils
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiObject
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||
import java.io.File
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
object TestUtils {
|
||||
private const val TAG = "TESTUTILS"
|
||||
@ -90,6 +93,31 @@ object TestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun ComposeContentTestRule.waitUntilTimeout(timeoutMillis: Long = TEST_PAUSE_MS.toLong()) {
|
||||
AsyncTimer.start(timeoutMillis)
|
||||
waitUntil(
|
||||
condition = { AsyncTimer.expired },
|
||||
timeoutMillis = timeoutMillis + 1000
|
||||
)
|
||||
}
|
||||
|
||||
object AsyncTimer {
|
||||
var expired = false
|
||||
fun start(delay: Long = 1000) {
|
||||
expired = false
|
||||
val timerTask = TimerTaskImpl {
|
||||
expired = true
|
||||
}
|
||||
Timer().schedule(timerTask, delay)
|
||||
}
|
||||
}
|
||||
|
||||
class TimerTaskImpl(private val runnable: Runnable) : TimerTask() {
|
||||
override fun run() {
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteTemporaryFilesOfTestCases(context: Context) {
|
||||
context.getExternalFilesDirs(null).filterNotNull()
|
||||
|
Loading…
x
Reference in New Issue
Block a user