mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-12 17:08:59 -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.Findable.ViewId
|
||||||
import org.kiwix.kiwixmobile.R
|
import org.kiwix.kiwixmobile.R
|
||||||
import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG
|
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.language.composables.LANGUAGE_ITEM_CHECKBOX_TESTING_TAG
|
||||||
import org.kiwix.kiwixmobile.nav.destination.library.online.LANGUAGE_MENU_ICON_TESTING_TAG
|
import org.kiwix.kiwixmobile.nav.destination.library.online.LANGUAGE_MENU_ICON_TESTING_TAG
|
||||||
import org.kiwix.kiwixmobile.testutils.TestUtils
|
import org.kiwix.kiwixmobile.testutils.TestUtils
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
package org.kiwix.kiwixmobile.search
|
package org.kiwix.kiwixmobile.search
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@ -25,7 +26,6 @@ import androidx.navigation.fragment.NavHostFragment
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.accessibility.AccessibilityChecks
|
import androidx.test.espresso.accessibility.AccessibilityChecks
|
||||||
import androidx.test.espresso.matcher.ViewMatchers
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
@ -47,11 +47,11 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.kiwix.kiwixmobile.BaseActivityTest
|
import org.kiwix.kiwixmobile.BaseActivityTest
|
||||||
import org.kiwix.kiwixmobile.R
|
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.SearchFragment
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||||
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
|
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
|
||||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
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.core.utils.TestingUtils.RETRY_RULE_ORDER
|
||||||
import org.kiwix.kiwixmobile.main.KiwixMainActivity
|
import org.kiwix.kiwixmobile.main.KiwixMainActivity
|
||||||
import org.kiwix.kiwixmobile.nav.destination.library.local.LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader
|
import org.kiwix.kiwixmobile.nav.destination.library.local.LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader
|
||||||
@ -73,6 +73,9 @@ class SearchFragmentTest : BaseActivityTest() {
|
|||||||
@JvmField
|
@JvmField
|
||||||
val retryRule = RetryRule()
|
val retryRule = RetryRule()
|
||||||
|
|
||||||
|
@get:Rule(order = COMPOSE_TEST_RULE_ORDER)
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
private lateinit var kiwixMainActivity: KiwixMainActivity
|
private lateinit var kiwixMainActivity: KiwixMainActivity
|
||||||
private lateinit var uiDevice: UiDevice
|
private lateinit var uiDevice: UiDevice
|
||||||
private lateinit var downloadingZimFile: File
|
private lateinit var downloadingZimFile: File
|
||||||
@ -115,10 +118,6 @@ class SearchFragmentTest : BaseActivityTest() {
|
|||||||
setRunChecksFromRootView(true)
|
setRunChecksFromRootView(true)
|
||||||
setSuppressingResultMatcher(
|
setSuppressingResultMatcher(
|
||||||
anyOf(
|
anyOf(
|
||||||
allOf(
|
|
||||||
matchesCheck(TouchTargetSizeCheck::class.java),
|
|
||||||
matchesViews(ViewMatchers.withId(id.menu_searchintext))
|
|
||||||
),
|
|
||||||
allOf(
|
allOf(
|
||||||
matchesCheck(TouchTargetSizeCheck::class.java),
|
matchesCheck(TouchTargetSizeCheck::class.java),
|
||||||
matchesViews(
|
matchesViews(
|
||||||
@ -142,28 +141,32 @@ class SearchFragmentTest : BaseActivityTest() {
|
|||||||
search { checkZimFileSearchSuccessful(R.id.readerFragment) }
|
search { checkZimFileSearchSuccessful(R.id.readerFragment) }
|
||||||
openSearchWithQuery("Android", testZimFile)
|
openSearchWithQuery("Android", testZimFile)
|
||||||
search {
|
search {
|
||||||
clickOnSearchItemInSearchList()
|
clickOnSearchItemInSearchList(composeTestRule)
|
||||||
checkZimFileSearchSuccessful(R.id.readerFragment)
|
checkZimFileSearchSuccessful(R.id.readerFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
openSearchWithQuery(zimFile = testZimFile)
|
openSearchWithQuery(zimFile = testZimFile)
|
||||||
search {
|
search {
|
||||||
// test with fast typing/deleting
|
// test with fast typing/deleting
|
||||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery)
|
searchWithFrequentlyTypedWords(searchUnitTestingQuery, composeTestRule = composeTestRule)
|
||||||
assertSearchSuccessful(searchUnitTestResult)
|
assertSearchSuccessful(searchUnitTestResult, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice)
|
deleteSearchedQueryFrequently(
|
||||||
|
searchUnitTestingQuery,
|
||||||
|
uiDevice,
|
||||||
|
composeTestRule = composeTestRule
|
||||||
|
)
|
||||||
|
|
||||||
// test with a short delay typing/deleting to
|
// test with a short delay typing/deleting to
|
||||||
// properly test the cancelling of previously searching task
|
// properly test the cancelling of previously searching task
|
||||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50)
|
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50, composeTestRule)
|
||||||
assertSearchSuccessful(searchUnitTestResult)
|
assertSearchSuccessful(searchUnitTestResult, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50)
|
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50, composeTestRule)
|
||||||
|
|
||||||
// test with a long delay typing/deleting to
|
// test with a long delay typing/deleting to
|
||||||
// properly execute the search query letter by letter
|
// properly execute the search query letter by letter
|
||||||
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300)
|
searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300, composeTestRule)
|
||||||
assertSearchSuccessful(searchUnitTestResult)
|
assertSearchSuccessful(searchUnitTestResult, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300)
|
deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300, composeTestRule)
|
||||||
// to close the keyboard
|
// to close the keyboard
|
||||||
pressBack()
|
pressBack()
|
||||||
// go to reader screen
|
// go to reader screen
|
||||||
@ -190,39 +193,46 @@ class SearchFragmentTest : BaseActivityTest() {
|
|||||||
openSearchWithQuery(zimFile = downloadingZimFile)
|
openSearchWithQuery(zimFile = downloadingZimFile)
|
||||||
search {
|
search {
|
||||||
// test with fast typing/deleting
|
// test with fast typing/deleting
|
||||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile)
|
searchWithFrequentlyTypedWords(
|
||||||
assertSearchSuccessful(searchResultForDownloadedZimFile)
|
searchQueryForDownloadedZimFile,
|
||||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice)
|
composeTestRule = composeTestRule
|
||||||
|
)
|
||||||
|
assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule)
|
||||||
|
deleteSearchedQueryFrequently(
|
||||||
|
searchQueryForDownloadedZimFile,
|
||||||
|
uiDevice,
|
||||||
|
composeTestRule = composeTestRule
|
||||||
|
)
|
||||||
|
|
||||||
// test with a short delay typing/deleting to
|
// test with a short delay typing/deleting to
|
||||||
// properly test the cancelling of previously searching task
|
// properly test the cancelling of previously searching task
|
||||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50)
|
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50, composeTestRule)
|
||||||
assertSearchSuccessful(searchResultForDownloadedZimFile)
|
assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50)
|
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50, composeTestRule)
|
||||||
|
|
||||||
// test with a long delay typing/deleting to
|
// test with a long delay typing/deleting to
|
||||||
// properly execute the search query letter by letter
|
// properly execute the search query letter by letter
|
||||||
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300)
|
searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300, composeTestRule)
|
||||||
assertSearchSuccessful(searchResultForDownloadedZimFile)
|
assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300)
|
deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300, composeTestRule)
|
||||||
// open the reader fragment for next text case.
|
// 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
|
// Added test for checking the crash scenario where the application was crashing when we
|
||||||
// frequently searched for article, and clicked on the searched item.
|
// frequently searched for article, and clicked on the searched item.
|
||||||
search {
|
search {
|
||||||
// test by searching 10 article and clicking on them
|
// test by searching 10 article and clicking on them
|
||||||
searchAndClickOnArticle(searchQueryForDownloadedZimFile)
|
searchAndClickOnArticle(searchQueryForDownloadedZimFile, composeTestRule)
|
||||||
searchAndClickOnArticle("A Song")
|
searchAndClickOnArticle("A Song", composeTestRule)
|
||||||
searchAndClickOnArticle("The Ra")
|
searchAndClickOnArticle("The Ra", composeTestRule)
|
||||||
searchAndClickOnArticle("The Ge")
|
searchAndClickOnArticle("The Ge", composeTestRule)
|
||||||
searchAndClickOnArticle("Wish")
|
searchAndClickOnArticle("Wish", composeTestRule)
|
||||||
searchAndClickOnArticle("WIFI")
|
searchAndClickOnArticle("WIFI", composeTestRule)
|
||||||
searchAndClickOnArticle("Woman")
|
searchAndClickOnArticle("Woman", composeTestRule)
|
||||||
searchAndClickOnArticle("Big Ba")
|
searchAndClickOnArticle("Big Ba", composeTestRule)
|
||||||
searchAndClickOnArticle("My Wor")
|
searchAndClickOnArticle("My Wor", composeTestRule)
|
||||||
searchAndClickOnArticle("100")
|
searchAndClickOnArticle("100", composeTestRule)
|
||||||
assertArticleLoaded()
|
assertArticleLoaded()
|
||||||
}
|
}
|
||||||
removeTemporaryZimFilesToFreeUpDeviceStorage()
|
removeTemporaryZimFilesToFreeUpDeviceStorage()
|
||||||
|
@ -19,28 +19,33 @@
|
|||||||
package org.kiwix.kiwixmobile.search
|
package org.kiwix.kiwixmobile.search
|
||||||
|
|
||||||
import android.view.KeyEvent
|
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.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions.clearText
|
|
||||||
import androidx.test.espresso.action.ViewActions.click
|
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.withId
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
|
||||||
import androidx.test.espresso.web.sugar.Web.onWebView
|
import androidx.test.espresso.web.sugar.Web.onWebView
|
||||||
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
|
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
|
||||||
import androidx.test.espresso.web.webdriver.Locator
|
import androidx.test.espresso.web.webdriver.Locator
|
||||||
import androidx.test.uiautomator.UiDevice
|
import androidx.test.uiautomator.UiDevice
|
||||||
import applyWithViewHierarchyPrinting
|
import applyWithViewHierarchyPrinting
|
||||||
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
|
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.BaseRobot
|
||||||
import org.kiwix.kiwixmobile.Findable.ViewId
|
import org.kiwix.kiwixmobile.Findable.ViewId
|
||||||
import org.kiwix.kiwixmobile.core.R
|
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
|
||||||
|
import org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS
|
||||||
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
|
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
|
||||||
|
import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout
|
||||||
|
|
||||||
fun search(func: SearchRobot.() -> Unit) = SearchRobot().applyWithViewHierarchyPrinting(func)
|
fun search(func: SearchRobot.() -> Unit) = SearchRobot().applyWithViewHierarchyPrinting(func)
|
||||||
|
|
||||||
@ -50,15 +55,11 @@ class SearchRobot : BaseRobot() {
|
|||||||
val searchQueryForDownloadedZimFile = "A Fool"
|
val searchQueryForDownloadedZimFile = "A Fool"
|
||||||
val searchResultForDownloadedZimFile = "A Fool for You"
|
val searchResultForDownloadedZimFile = "A Fool for You"
|
||||||
|
|
||||||
fun clickOnSearchItemInSearchList() {
|
fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) {
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
composeTestRule.apply {
|
||||||
isVisible(ViewId(R.id.search_list))
|
waitUntilTimeout()
|
||||||
onView(withId(R.id.search_list)).perform(
|
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick()
|
||||||
actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
}
|
||||||
0,
|
|
||||||
click()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkZimFileSearchSuccessful(readerFragment: Int) {
|
fun checkZimFileSearchSuccessful(readerFragment: Int) {
|
||||||
@ -66,31 +67,47 @@ class SearchRobot : BaseRobot() {
|
|||||||
isVisible(ViewId(readerFragment))
|
isVisible(ViewId(readerFragment))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) {
|
fun searchWithFrequentlyTypedWords(
|
||||||
|
query: String,
|
||||||
|
wait: Long = 0L,
|
||||||
|
composeTestRule: ComposeContentTestRule
|
||||||
|
) {
|
||||||
testFlakyView({
|
testFlakyView({
|
||||||
val searchView = onView(withId(androidx.appcompat.R.id.search_src_text))
|
composeTestRule.apply {
|
||||||
for (char in query) {
|
waitUntilTimeout()
|
||||||
searchView.perform(typeText(char.toString()))
|
val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG)
|
||||||
if (wait != 0L) {
|
searchView.performTextInput("")
|
||||||
BaristaSleepInteractions.sleep(wait)
|
for (char in query) {
|
||||||
|
searchView.performTextInput(char.toString())
|
||||||
|
if (wait != 0L) {
|
||||||
|
waitUntilTimeout(wait)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertSearchSuccessful(searchResult: String) {
|
fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) {
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
composeTestRule.apply {
|
||||||
val recyclerViewId = R.id.search_list
|
waitUntil(
|
||||||
|
timeoutMillis = TEST_PAUSE_MS.toLong(),
|
||||||
onView(withId(recyclerViewId)).check(
|
condition = {
|
||||||
matches(
|
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)
|
||||||
atPosition(0, hasDescendant(withText(searchResult)))
|
.fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0]
|
||||||
|
.assert(hasText(searchResult))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) {
|
fun deleteSearchedQueryFrequently(
|
||||||
for (i in textToDelete.indices) {
|
textToDelete: String,
|
||||||
|
uiDevice: UiDevice,
|
||||||
|
wait: Long = 0L,
|
||||||
|
composeTestRule: ComposeContentTestRule
|
||||||
|
) {
|
||||||
|
repeat(textToDelete.length) {
|
||||||
uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL)
|
uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL)
|
||||||
if (wait != 0L) {
|
if (wait != 0L) {
|
||||||
BaristaSleepInteractions.sleep(wait)
|
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
|
// 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))
|
composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance()
|
||||||
searchView.perform(clearText())
|
}
|
||||||
|
|
||||||
|
fun clickOnNavigationIcon(composeTestRule: ComposeContentTestRule) {
|
||||||
|
composeTestRule.onNodeWithTag(NAVIGATION_ICON_TESTING_TAG).performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSearchScreen() {
|
private fun openSearchScreen() {
|
||||||
testFlakyView({ onView(withId(R.id.menu_search)).perform(click()) })
|
testFlakyView({ onView(withId(R.id.menu_search)).perform(click()) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchAndClickOnArticle(searchString: String) {
|
fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) {
|
||||||
// wait a bit to properly load the ZIM file in the reader
|
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
|
||||||
openSearchScreen()
|
openSearchScreen()
|
||||||
searchWithFrequentlyTypedWords(searchString)
|
searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule)
|
||||||
clickOnSearchItemInSearchList()
|
clickOnSearchItemInSearchList(composeTestRule)
|
||||||
|
checkZimFileSearchSuccessful(org.kiwix.kiwixmobile.R.id.readerFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertArticleLoaded() {
|
fun assertArticleLoaded() {
|
||||||
|
@ -47,7 +47,6 @@ import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val SAVE_ICON_TESTING_TAG = "saveLanguages"
|
const val SAVE_ICON_TESTING_TAG = "saveLanguages"
|
||||||
const val SEARCH_FIELD_TESTING_TAG = "searchField"
|
|
||||||
|
|
||||||
class LanguageFragment : BaseFragment() {
|
class LanguageFragment : BaseFragment() {
|
||||||
private val languageViewModel by lazy { viewModel<LanguageViewModel>(viewModelFactory) }
|
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 androidx.compose.ui.unit.dp
|
||||||
import org.kiwix.kiwixmobile.core.R
|
import org.kiwix.kiwixmobile.core.R
|
||||||
import org.kiwix.kiwixmobile.core.extensions.CollectSideEffectWithActivity
|
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.ContentLoadingProgressBar
|
||||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
|
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
|
||||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView
|
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.semantics.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.R.string
|
||||||
import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll
|
import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll
|
||||||
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
|
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.FOUR_DP
|
||||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_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.SIX_DP
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.THREE_DP
|
||||||
import org.kiwix.kiwixmobile.nav.destination.library.local.rememberScrollBehavior
|
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
|
||||||
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
|
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
|
||||||
@ -258,7 +258,7 @@ private fun ShowFetchingLibraryLayout(message: String) {
|
|||||||
) {
|
) {
|
||||||
ContentLoadingProgressBar(
|
ContentLoadingProgressBar(
|
||||||
modifier = Modifier.size(DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE),
|
modifier = Modifier.size(DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE),
|
||||||
circularProgressBarStockWidth = 3.dp,
|
circularProgressBarStockWidth = THREE_DP,
|
||||||
progressBarTrackColor = cardContainerColor
|
progressBarTrackColor = cardContainerColor
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
@ -132,8 +132,7 @@
|
|||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/searchFragment"
|
android:id="@+id/searchFragment"
|
||||||
android:name="org.kiwix.kiwixmobile.core.search.SearchFragment"
|
android:name="org.kiwix.kiwixmobile.core.search.SearchFragment"
|
||||||
android:label="SearchFragment"
|
android:label="SearchFragment">
|
||||||
tools:layout="@layout/fragment_search">
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_searchFragment_to_readerFragment"
|
android:id="@+id/action_searchFragment_to_readerFragment"
|
||||||
app:destination="@id/readerFragment"
|
app:destination="@id/readerFragment"
|
||||||
|
@ -23,7 +23,7 @@ import io.objectbox.query.QueryBuilder
|
|||||||
import kotlinx.coroutines.flow.map
|
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.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
|
import javax.inject.Inject
|
||||||
|
|
||||||
class NewRecentSearchDao @Inject constructor(
|
class NewRecentSearchDao @Inject constructor(
|
||||||
|
@ -23,7 +23,7 @@ import androidx.room.Query
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchRoomEntity
|
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchRoomEntity
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class RecentSearchRoomDao {
|
abstract class RecentSearchRoomDao {
|
||||||
|
@ -17,30 +17,19 @@
|
|||||||
*/
|
*/
|
||||||
package org.kiwix.kiwixmobile.core.search
|
package org.kiwix.kiwixmobile.core.search
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.core.view.MenuHost
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.core.view.MenuProvider
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.widget.ContentLoadingProgressBar
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -51,22 +40,15 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.kiwix.kiwixmobile.core.R
|
import org.kiwix.kiwixmobile.core.R
|
||||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||||
import org.kiwix.kiwixmobile.core.base.BaseFragment
|
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.ActivityExtensions.cachedComponent
|
||||||
import org.kiwix.kiwixmobile.core.extensions.closeKeyboard
|
import org.kiwix.kiwixmobile.core.extensions.closeKeyboard
|
||||||
import org.kiwix.kiwixmobile.core.extensions.coreMainActivity
|
import org.kiwix.kiwixmobile.core.extensions.coreMainActivity
|
||||||
import org.kiwix.kiwixmobile.core.extensions.getDialogHostComposeView
|
import org.kiwix.kiwixmobile.core.extensions.update
|
||||||
import org.kiwix.kiwixmobile.core.extensions.setUpSearchView
|
|
||||||
import org.kiwix.kiwixmobile.core.extensions.viewModel
|
import org.kiwix.kiwixmobile.core.extensions.viewModel
|
||||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
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
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ActivityResultReceived
|
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.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.Filter
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemClick
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemClick
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemLongClick
|
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.SearchOrigin.FromWebView
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchState
|
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchState
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchViewModel
|
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.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.AlertDialogShower
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.dialog.DialogHost
|
||||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val NAV_ARG_SEARCH_STRING = "searchString"
|
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() {
|
class SearchFragment : BaseFragment() {
|
||||||
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
|
@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
|
@Inject lateinit var dialogShower: DialogShower
|
||||||
|
|
||||||
val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
|
val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
|
||||||
private var searchAdapter: SearchAdapter? = null
|
private var isDataLoading = mutableStateOf(false)
|
||||||
private var isDataLoading = false
|
|
||||||
private var renderingJob: Job? = null
|
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) {
|
override fun inject(baseActivity: BaseActivity) {
|
||||||
baseActivity.cachedComponent.inject(this)
|
baseActivity.cachedComponent.inject(this)
|
||||||
}
|
}
|
||||||
@ -111,80 +116,87 @@ class SearchFragment : BaseFragment() {
|
|||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? = ComposeView(requireContext()).also {
|
||||||
fragmentSearchBinding = FragmentSearchBinding.inflate(inflater, container, false)
|
composeView = it
|
||||||
setupMenu()
|
|
||||||
return fragmentSearchBinding?.root
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
searchAdapter = SearchAdapter(
|
composeView?.apply {
|
||||||
RecentSearchDelegate(::onItemClick, ::onItemClickNewTab) {
|
setContent {
|
||||||
searchViewModel.actions.trySend(OnItemLongClick(it)).isSuccess
|
SearchScreen(
|
||||||
},
|
searchScreenState.value,
|
||||||
ZimSearchResultDelegate(::onItemClick, ::onItemClickNewTab)
|
actionMenuItems(),
|
||||||
)
|
isDataLoading.value
|
||||||
setupToolbar(view)
|
)
|
||||||
fragmentSearchBinding?.searchList?.run {
|
DialogHost(dialogShower as AlertDialogShower)
|
||||||
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) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleBackPress()
|
|
||||||
fragmentSearchBinding?.root?.addView(
|
|
||||||
requireContext().getDialogHostComposeView(dialogShower as AlertDialogShower)
|
|
||||||
)
|
|
||||||
searchViewModel.setAlertDialogShower(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.
|
* 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.
|
* This function is typically triggered when the RecyclerView is near about its last item.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
private fun loadMoreSearchResult() {
|
private fun loadMoreSearchResult() {
|
||||||
if (isDataLoading) return
|
if (isDataLoading.value) return
|
||||||
isDataLoading = true
|
isDataLoading.value = true
|
||||||
val safeStartIndex = searchAdapter?.itemCount ?: 0
|
val searchList = searchScreenState.value.searchList
|
||||||
// Show a loading indicator while data is being loaded
|
// Show a loading indicator while data is being loaded
|
||||||
fragmentSearchBinding?.loadingMoreDataIndicator?.isShowing(true)
|
searchScreenState.update { copy(shouldShowLoadingMoreProgressBar = true) }
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Request more search results from the ViewModel, providing the start
|
// Request more search results from the ViewModel, providing the start
|
||||||
// index and existing results
|
// index and existing results
|
||||||
searchViewModel.loadMoreSearchResults(safeStartIndex, searchAdapter?.items)
|
searchViewModel.loadMoreSearchResults(searchList.size, searchList)
|
||||||
.let { searchResults ->
|
.let { searchResults ->
|
||||||
// Hide the loading indicator when data loading is complete
|
// 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
|
// Update data loading status based on the received search results
|
||||||
isDataLoading = when {
|
isDataLoading.value = when {
|
||||||
searchResults == null -> true
|
searchResults == null -> true
|
||||||
searchResults.isEmpty() -> false
|
searchResults.isEmpty() -> false
|
||||||
else -> {
|
else -> {
|
||||||
// Append the new search results to the existing list
|
// Append the new search results to the existing list
|
||||||
searchAdapter?.addData(searchResults)
|
searchScreenState.update {
|
||||||
|
copy(searchList = searchScreenState.value.searchList + searchResults)
|
||||||
|
}
|
||||||
false
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
renderingJob?.cancel()
|
renderingJob?.cancel()
|
||||||
renderingJob = null
|
renderingJob = null
|
||||||
activity?.intent?.action = null
|
activity?.intent?.action = null
|
||||||
searchView?.setOnQueryTextListener(null)
|
composeView?.disposeComposition()
|
||||||
searchView = null
|
composeView = null
|
||||||
searchInTextMenuItem = null
|
|
||||||
findInPageTextView = null
|
|
||||||
searchMenuItem?.setOnActionExpandListener(null)
|
|
||||||
searchMenuItem = null
|
|
||||||
fragmentSearchBinding?.searchList?.adapter = null
|
|
||||||
searchAdapter = null
|
|
||||||
fragmentSearchBinding?.root?.removeAllViews()
|
|
||||||
fragmentSearchBinding = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goBack() {
|
private fun goBack() {
|
||||||
@ -238,84 +229,48 @@ class SearchFragment : BaseFragment() {
|
|||||||
findNavController().popBackStack(readerFragmentResId, false)
|
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? =
|
private fun getSearchListItemForQuery(query: String): SearchListItem? =
|
||||||
searchAdapter?.items?.firstOrNull {
|
searchScreenState.value.searchList.firstOrNull {
|
||||||
it.value.equals(query, ignoreCase = true)
|
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")
|
@Suppress("InjectDispatcher")
|
||||||
suspend fun render(state: SearchState) {
|
private suspend fun render(state: SearchState) {
|
||||||
renderingJob?.apply {
|
renderingJob?.apply {
|
||||||
// cancel the children job. Since we are getting the result on IO thread
|
// cancel the children job. Since we are getting the result on IO thread
|
||||||
// with `withContext` that is child for this job
|
// with `withContext` that is child for this job
|
||||||
@ -334,11 +289,10 @@ class SearchFragment : BaseFragment() {
|
|||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isDataLoading = false
|
isDataLoading.value = false
|
||||||
searchInTextMenuItem?.actionView?.isVisible = state.searchOrigin == FromWebView
|
findInPageMenuItem.value = findInPageMenuItem.value.first to (state.searchOrigin == FromWebView)
|
||||||
setIsPageSearchEnabled(state.searchTerm.isNotBlank())
|
setIsPageSearchEnabled(state.searchTerm)
|
||||||
|
searchScreenState.update { copy(isLoading = true) }
|
||||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(true)
|
|
||||||
renderingJob =
|
renderingJob =
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
@ -347,10 +301,11 @@ class SearchFragment : BaseFragment() {
|
|||||||
state.getVisibleResults(0, coroutineContext[Job])
|
state.getVisibleResults(0, coroutineContext[Job])
|
||||||
}
|
}
|
||||||
|
|
||||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(false)
|
searchScreenState.update { copy(isLoading = false) }
|
||||||
searchResult?.let {
|
searchResult?.let {
|
||||||
fragmentSearchBinding?.searchNoResults?.isVisible = it.isEmpty()
|
searchScreenState.update {
|
||||||
searchAdapter?.items = it
|
copy(searchList = it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (ignore: CancellationException) {
|
} catch (ignore: CancellationException) {
|
||||||
Log.e("SEARCH_RESULT", "Cancelled the previous job ${ignore.message}")
|
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}"
|
"Error in getting searched result\nOriginal exception ${ignore.message}"
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(false)
|
searchScreenState.update { copy(isLoading = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setIsPageSearchEnabled(isEnabled: Boolean) {
|
private fun setIsPageSearchEnabled(searchText: String) {
|
||||||
searchInTextMenuItem?.actionView?.isEnabled = isEnabled
|
findInPageMenuItem.value = searchText.isNotBlank() to findInPageMenuItem.value.second
|
||||||
findInPageTextView?.alpha =
|
|
||||||
if (isEnabled) {
|
|
||||||
ENABLED_SEARCH_IN_TEXT_OPACITY
|
|
||||||
} else {
|
|
||||||
DISABLED_SEARCH_IN_TEXT_OPACITY
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemClick(it: SearchListItem) {
|
private fun onItemClick(it: SearchListItem) {
|
||||||
@ -388,14 +337,12 @@ class SearchFragment : BaseFragment() {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
searchViewModel.actions.trySend(ActivityResultReceived(requestCode, resultCode, data)).isSuccess
|
searchViewModel.actions.trySend(
|
||||||
}
|
ActivityResultReceived(
|
||||||
}
|
requestCode,
|
||||||
|
resultCode,
|
||||||
private fun ContentLoadingProgressBar.isShowing(show: Boolean) {
|
data
|
||||||
if (show) {
|
)
|
||||||
show()
|
).isSuccess
|
||||||
} else {
|
|
||||||
hide()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Kiwix Android
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* 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 {
|
sealed class SearchListItem {
|
||||||
abstract val value: String
|
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.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||||
|
|
||||||
sealed class Action {
|
sealed class Action {
|
||||||
object ExitedSearch : Action()
|
object ExitedSearch : Action()
|
||||||
|
@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.core.search.viewmodel
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.yield
|
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
|
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||||
|
|
||||||
data class SearchState(
|
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.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
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.ActivityResultReceived
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
||||||
@ -84,7 +84,7 @@ class SearchViewModel @Inject constructor(
|
|||||||
FromWebView
|
FromWebView
|
||||||
)
|
)
|
||||||
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
||||||
private val _effects = Channel<SideEffect<*>>()
|
private val _effects = Channel<SideEffect<*>>(Channel.UNLIMITED)
|
||||||
val effects = _effects.receiveAsFlow()
|
val effects = _effects.receiveAsFlow()
|
||||||
val actions = Channel<Action>(Channel.UNLIMITED)
|
val actions = Channel<Action>(Channel.UNLIMITED)
|
||||||
private val filter = MutableStateFlow("")
|
private val filter = MutableStateFlow("")
|
||||||
|
@ -24,7 +24,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||||
|
|
||||||
@Suppress("InjectDispatcher")
|
@Suppress("InjectDispatcher")
|
||||||
data class DeleteRecentSearch(
|
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.CoreMainActivity
|
||||||
import org.kiwix.kiwixmobile.core.main.SEARCH_ITEM_TITLE_KEY
|
import org.kiwix.kiwixmobile.core.main.SEARCH_ITEM_TITLE_KEY
|
||||||
import org.kiwix.kiwixmobile.core.reader.addContentPrefix
|
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
|
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED
|
||||||
|
|
||||||
data class OpenSearchItem(
|
data class OpenSearchItem(
|
||||||
|
@ -25,7 +25,7 @@ import kotlinx.coroutines.launch
|
|||||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||||
import org.kiwix.kiwixmobile.core.reader.addContentPrefix
|
import org.kiwix.kiwixmobile.core.reader.addContentPrefix
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||||
|
|
||||||
@Suppress("InjectDispatcher")
|
@Suppress("InjectDispatcher")
|
||||||
data class SaveSearchToRecents(
|
data class SaveSearchToRecents(
|
||||||
|
@ -22,7 +22,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
|
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.search.viewmodel.Action
|
||||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
||||||
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSearch
|
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
|
REQ_CODE_SPEECH_INPUT
|
||||||
)
|
)
|
||||||
} catch (a: ActivityNotFoundException) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
actions.trySend(StartSpeechInputFailed).isSuccess
|
actions.trySend(StartSpeechInputFailed).isSuccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
@ -135,16 +136,34 @@ private fun AppBarTitle(
|
|||||||
private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
|
private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
|
||||||
Row {
|
Row {
|
||||||
actionMenuItems.forEach { menuItem ->
|
actionMenuItems.forEach { menuItem ->
|
||||||
IconButton(
|
val modifier = menuItem.modifier.testTag(menuItem.testingTag)
|
||||||
enabled = menuItem.isEnabled,
|
// If icon is not null show the icon.
|
||||||
onClick = menuItem.onClick,
|
menuItem.icon?.let {
|
||||||
modifier = menuItem.modifier.testTag(menuItem.testingTag)
|
IconButton(
|
||||||
) {
|
enabled = menuItem.isEnabled,
|
||||||
Icon(
|
onClick = menuItem.onClick,
|
||||||
painter = menuItem.icon.toPainter(),
|
modifier = modifier
|
||||||
contentDescription = stringResource(menuItem.contentDescription),
|
) {
|
||||||
tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray
|
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
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Text
|
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.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import org.kiwix.kiwixmobile.core.R
|
import org.kiwix.kiwixmobile.core.R
|
||||||
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
|
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
|
||||||
|
|
||||||
@ -45,8 +49,10 @@ fun KiwixSearchView(
|
|||||||
searchViewTextFiledTestTag: String = "",
|
searchViewTextFiledTestTag: String = "",
|
||||||
clearButtonTestTag: String = "",
|
clearButtonTestTag: String = "",
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
onClearClick: () -> Unit
|
onClearClick: () -> Unit,
|
||||||
|
onKeyboardSubmitButtonClick: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val colors = TextFieldDefaults.colors(
|
val colors = TextFieldDefaults.colors(
|
||||||
focusedIndicatorColor = Color.Transparent,
|
focusedIndicatorColor = Color.Transparent,
|
||||||
unfocusedIndicatorColor = 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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
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.R
|
||||||
import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
||||||
import org.kiwix.kiwixmobile.core.ui.models.toPainter
|
import org.kiwix.kiwixmobile.core.ui.models.toPainter
|
||||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
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
|
* A composable function that renders a navigation icon, which can be either a vector
|
||||||
* or drawable image.
|
* or drawable image.
|
||||||
@ -47,9 +52,10 @@ fun NavigationIcon(
|
|||||||
iconItem: IconItem = IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack),
|
iconItem: IconItem = IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack),
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
@StringRes contentDescription: Int = R.string.toolbar_back_button_content_description,
|
@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(
|
Icon(
|
||||||
painter = iconItem.toPainter(),
|
painter = iconItem.toPainter(),
|
||||||
contentDescription = stringResource(contentDescription),
|
contentDescription = stringResource(contentDescription),
|
||||||
|
@ -21,14 +21,16 @@ package org.kiwix.kiwixmobile.core.ui.models
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import org.kiwix.kiwixmobile.core.R
|
||||||
import org.kiwix.kiwixmobile.core.ui.theme.White
|
import org.kiwix.kiwixmobile.core.ui.theme.White
|
||||||
|
|
||||||
data class ActionMenuItem(
|
data class ActionMenuItem(
|
||||||
val icon: IconItem,
|
val icon: IconItem? = null,
|
||||||
@StringRes val contentDescription: Int,
|
@StringRes val contentDescription: Int,
|
||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
val iconTint: Color = White,
|
val iconTint: Color = White,
|
||||||
val isEnabled: Boolean = true,
|
val isEnabled: Boolean = true,
|
||||||
|
@StringRes val iconButtonText: Int = R.string.empty_string,
|
||||||
val testingTag: String,
|
val testingTag: String,
|
||||||
val modifier: Modifier = Modifier
|
val modifier: Modifier = Modifier
|
||||||
)
|
)
|
||||||
|
@ -35,8 +35,8 @@ object ComposeDimens {
|
|||||||
// Error screen dimens
|
// Error screen dimens
|
||||||
val CRASH_IMAGE_SIZE = 70.dp
|
val CRASH_IMAGE_SIZE = 70.dp
|
||||||
|
|
||||||
// KiwixAppBar(Toolbar) height
|
// KiwixAppBar(Toolbar) dimens
|
||||||
val KIWIX_APP_BAR_HEIGHT = 56.dp
|
val ACTION_MENU_TEXTVIEW_BUTTON_PADDING = 13.dp
|
||||||
|
|
||||||
// Padding & Margins
|
// Padding & Margins
|
||||||
val SIXTY_DP = 60.dp
|
val SIXTY_DP = 60.dp
|
||||||
@ -51,6 +51,7 @@ object ComposeDimens {
|
|||||||
val SIX_DP = 6.dp
|
val SIX_DP = 6.dp
|
||||||
val FIVE_DP = 5.dp
|
val FIVE_DP = 5.dp
|
||||||
val FOUR_DP = 4.dp
|
val FOUR_DP = 4.dp
|
||||||
|
val THREE_DP = 3.dp
|
||||||
val TWO_DP = 2.dp
|
val TWO_DP = 2.dp
|
||||||
val ONE_DP = 1.dp
|
val ONE_DP = 1.dp
|
||||||
val SEVENTY_DP = 70.dp
|
val SEVENTY_DP = 70.dp
|
||||||
@ -169,4 +170,9 @@ object ComposeDimens {
|
|||||||
val DOWNLOADING_LIBRARY_MESSAGE_TEXT_SIZE = 8.sp
|
val DOWNLOADING_LIBRARY_MESSAGE_TEXT_SIZE = 8.sp
|
||||||
val DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE = 30.dp
|
val DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE = 30.dp
|
||||||
const val ONLINE_BOOK_DISABLED_COLOR_ALPHA = 0.5F
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:popupTheme="@style/KiwixTheme"
|
app:popupTheme="@style/KiwixTheme"
|
||||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
|
||||||
tools:showIn="@layout/fragment_search">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/toolbarWithSearchPlaceholder"
|
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.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.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.kiwixmobile.core.search.viewmodel.test
|
||||||
import org.kiwix.sharedFunctions.recentSearchEntity
|
import org.kiwix.sharedFunctions.recentSearchEntity
|
||||||
|
|
||||||
|
@ -27,8 +27,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView
|
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView
|
||||||
|
|
||||||
internal class SearchStateTest {
|
internal class SearchStateTest {
|
||||||
@ -123,8 +122,8 @@ internal class SearchStateTest {
|
|||||||
val searchResultsWithTerm =
|
val searchResultsWithTerm =
|
||||||
SearchResultsWithTerm(searchTerm, suggestionSearchWrapper, mockk())
|
SearchResultsWithTerm(searchTerm, suggestionSearchWrapper, mockk())
|
||||||
val searchState = SearchState(searchTerm, searchResultsWithTerm, emptyList(), FromWebView)
|
val searchState = SearchState(searchTerm, searchResultsWithTerm, emptyList(), FromWebView)
|
||||||
var list: List<SearchListItem.RecentSearchListItem>? = emptyList()
|
var list: List<RecentSearchListItem>? = emptyList()
|
||||||
var list1: List<SearchListItem.RecentSearchListItem>? = emptyList()
|
var list1: List<RecentSearchListItem>? = emptyList()
|
||||||
val job =
|
val job =
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
delay(1000)
|
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.dao.RecentSearchRoomDao
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
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.ActivityResultReceived
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
||||||
|
@ -27,8 +27,8 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.SearchListItem
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem
|
||||||
|
|
||||||
internal class DeleteRecentSearchTest {
|
internal class DeleteRecentSearchTest {
|
||||||
@Test
|
@Test
|
||||||
|
@ -28,7 +28,7 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationResultOnCurrent
|
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationResultOnCurrent
|
||||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
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
|
||||||
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED_NEW_TAB
|
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.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
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 {
|
internal class SaveSearchToRecentsTest {
|
||||||
private val newRecentSearchDao: RecentSearchRoomDao = mockk()
|
private val newRecentSearchDao: RecentSearchRoomDao = mockk()
|
||||||
|
@ -25,7 +25,7 @@ import io.mockk.verify
|
|||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
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
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete
|
||||||
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
|
||||||
|
@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.custom.search
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.AssetFileDescriptor
|
import android.content.res.AssetFileDescriptor
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
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.search.viewmodel.Action
|
||||||
import org.kiwix.kiwixmobile.core.utils.LanguageUtils
|
import org.kiwix.kiwixmobile.core.utils.LanguageUtils
|
||||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
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.CustomMainActivity
|
||||||
import org.kiwix.kiwixmobile.custom.main.CustomReaderFragment
|
import org.kiwix.kiwixmobile.custom.main.CustomReaderFragment
|
||||||
import org.kiwix.kiwixmobile.custom.testutils.RetryRule
|
import org.kiwix.kiwixmobile.custom.testutils.RetryRule
|
||||||
@ -80,10 +83,13 @@ class SearchFragmentTestForCustomApp {
|
|||||||
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule(order = RETRY_RULE_ORDER)
|
||||||
@JvmField
|
@JvmField
|
||||||
var retryRule = RetryRule()
|
var retryRule = RetryRule()
|
||||||
|
|
||||||
|
@get:Rule(order = COMPOSE_TEST_RULE_ORDER)
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
private lateinit var customMainActivity: CustomMainActivity
|
private lateinit var customMainActivity: CustomMainActivity
|
||||||
private lateinit var uiDevice: UiDevice
|
private lateinit var uiDevice: UiDevice
|
||||||
private lateinit var downloadingZimFile: File
|
private lateinit var downloadingZimFile: File
|
||||||
@ -151,24 +157,24 @@ class SearchFragmentTestForCustomApp {
|
|||||||
openZimFileInReader(zimFile = downloadingZimFile)
|
openZimFileInReader(zimFile = downloadingZimFile)
|
||||||
openSearchWithQuery()
|
openSearchWithQuery()
|
||||||
val searchTerm = "gard"
|
val searchTerm = "gard"
|
||||||
val searchedItem = "Gardanta Spirito"
|
val searchedItem = "Gardanta Spirito - Андивионский Научный Альянс"
|
||||||
search {
|
search {
|
||||||
// test with fast typing/deleting
|
// test with fast typing/deleting
|
||||||
searchWithFrequentlyTypedWords(searchTerm)
|
searchWithFrequentlyTypedWords(searchTerm, composeTestRule = composeTestRule)
|
||||||
assertSearchSuccessful(searchedItem)
|
assertSearchSuccessful(searchedItem, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchTerm, uiDevice)
|
deleteSearchedQueryFrequently(searchTerm, uiDevice, composeTestRule = composeTestRule)
|
||||||
|
|
||||||
// test with a short delay typing/deleting to
|
// test with a short delay typing/deleting to
|
||||||
// properly test the cancelling of previously searching task
|
// properly test the cancelling of previously searching task
|
||||||
searchWithFrequentlyTypedWords(searchTerm, 50)
|
searchWithFrequentlyTypedWords(searchTerm, 50, composeTestRule)
|
||||||
assertSearchSuccessful(searchedItem)
|
assertSearchSuccessful(searchedItem, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchTerm, uiDevice, 50)
|
deleteSearchedQueryFrequently(searchTerm, uiDevice, 50, composeTestRule)
|
||||||
|
|
||||||
// test with a long delay typing/deleting to
|
// test with a long delay typing/deleting to
|
||||||
// properly execute the search query letter by letter
|
// properly execute the search query letter by letter
|
||||||
searchWithFrequentlyTypedWords(searchTerm, 300)
|
searchWithFrequentlyTypedWords(searchTerm, 300, composeTestRule)
|
||||||
assertSearchSuccessful(searchedItem)
|
assertSearchSuccessful(searchedItem, composeTestRule)
|
||||||
deleteSearchedQueryFrequently(searchTerm, uiDevice, 300)
|
deleteSearchedQueryFrequently(searchTerm, uiDevice, 300, composeTestRule)
|
||||||
// to close the keyboard
|
// to close the keyboard
|
||||||
pressBack()
|
pressBack()
|
||||||
// go to reader screen
|
// go to reader screen
|
||||||
@ -179,16 +185,16 @@ class SearchFragmentTestForCustomApp {
|
|||||||
// frequently searched for article, and clicked on the searched item.
|
// frequently searched for article, and clicked on the searched item.
|
||||||
search {
|
search {
|
||||||
// test by searching 10 article and clicking on them
|
// test by searching 10 article and clicking on them
|
||||||
searchAndClickOnArticle(searchTerm)
|
searchAndClickOnArticle(searchTerm, composeTestRule)
|
||||||
searchAndClickOnArticle("eilum")
|
searchAndClickOnArticle("eilum", composeTestRule)
|
||||||
searchAndClickOnArticle("page")
|
searchAndClickOnArticle("page", composeTestRule)
|
||||||
searchAndClickOnArticle("list")
|
searchAndClickOnArticle("list", composeTestRule)
|
||||||
searchAndClickOnArticle("ladder")
|
searchAndClickOnArticle("ladder", composeTestRule)
|
||||||
searchAndClickOnArticle("welc")
|
searchAndClickOnArticle("welc", composeTestRule)
|
||||||
searchAndClickOnArticle("js")
|
searchAndClickOnArticle("js", composeTestRule)
|
||||||
searchAndClickOnArticle("hizo")
|
searchAndClickOnArticle("hizo", composeTestRule)
|
||||||
searchAndClickOnArticle("fad")
|
searchAndClickOnArticle("fad", composeTestRule)
|
||||||
searchAndClickOnArticle("forum")
|
searchAndClickOnArticle("forum", composeTestRule)
|
||||||
assertArticleLoaded()
|
assertArticleLoaded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,14 +19,19 @@
|
|||||||
package org.kiwix.kiwixmobile.custom.search
|
package org.kiwix.kiwixmobile.custom.search
|
||||||
|
|
||||||
import android.view.KeyEvent
|
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.core.view.GravityCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.action.ViewActions.click
|
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
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
|
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 androidx.test.uiautomator.UiDevice
|
||||||
import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawerWithGravity
|
import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawerWithGravity
|
||||||
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
|
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
|
||||||
import com.adevinta.android.barista.internal.matcher.HelperMatchers
|
|
||||||
import org.hamcrest.CoreMatchers.containsString
|
import org.hamcrest.CoreMatchers.containsString
|
||||||
import org.kiwix.kiwixmobile.core.R
|
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.R.id
|
||||||
import org.kiwix.kiwixmobile.custom.testutils.TestUtils
|
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.testFlakyView
|
||||||
|
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.waitUntilTimeout
|
||||||
|
|
||||||
fun search(searchRobot: SearchRobot.() -> Unit) = SearchRobot().searchRobot()
|
fun search(searchRobot: SearchRobot.() -> Unit) = SearchRobot().searchRobot()
|
||||||
|
|
||||||
class SearchRobot {
|
class SearchRobot {
|
||||||
fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) {
|
fun searchWithFrequentlyTypedWords(
|
||||||
|
query: String,
|
||||||
|
wait: Long = 0L,
|
||||||
|
composeTestRule: ComposeContentTestRule
|
||||||
|
) {
|
||||||
testFlakyView({
|
testFlakyView({
|
||||||
val searchView = Espresso.onView(ViewMatchers.withId(androidx.appcompat.R.id.search_src_text))
|
composeTestRule.apply {
|
||||||
searchView.perform(ViewActions.clearText())
|
waitUntilTimeout()
|
||||||
for (char in query) {
|
val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG)
|
||||||
searchView.perform(ViewActions.typeText(char.toString()))
|
searchView.performTextInput("")
|
||||||
if (wait != 0L) {
|
for (char in query) {
|
||||||
BaristaSleepInteractions.sleep(wait)
|
searchView.performTextInput(char.toString())
|
||||||
|
if (wait != 0L) {
|
||||||
|
waitUntilTimeout(wait)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertSearchSuccessful(searchResult: String) {
|
fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) {
|
||||||
testFlakyView({
|
composeTestRule.apply {
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
waitUntil(
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.search_list)).check(
|
timeoutMillis = TEST_PAUSE_MS.toLong(),
|
||||||
ViewAssertions.matches(
|
condition = {
|
||||||
HelperMatchers.atPosition(
|
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)
|
||||||
0,
|
.fetchSemanticsNodes().isNotEmpty()
|
||||||
ViewMatchers.hasDescendant(ViewMatchers.withSubstring(searchResult))
|
}
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
})
|
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0]
|
||||||
|
.assert(hasText(searchResult))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) {
|
fun deleteSearchedQueryFrequently(
|
||||||
testFlakyView({
|
textToDelete: String,
|
||||||
for (i in textToDelete.indices) {
|
uiDevice: UiDevice,
|
||||||
uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL)
|
wait: Long = 0L,
|
||||||
if (wait != 0L) {
|
composeTestRule: ComposeContentTestRule
|
||||||
BaristaSleepInteractions.sleep(wait)
|
) {
|
||||||
}
|
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
|
// 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))
|
composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance()
|
||||||
searchView.perform(ViewActions.clearText())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openSearchScreen() {
|
private fun openSearchScreen() {
|
||||||
@ -99,26 +116,21 @@ class SearchRobot {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchAndClickOnArticle(searchString: String) {
|
fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) {
|
||||||
// Wait a bit to properly load the ZIM file in the reader.
|
// wait a bit to properly load the ZIM file in the reader
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
composeTestRule.waitUntilTimeout()
|
||||||
openSearchScreen()
|
openSearchScreen()
|
||||||
// Wait a bit to properly visible the search screen.
|
// Wait a bit to properly visible the search screen.
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
composeTestRule.waitUntilTimeout()
|
||||||
searchWithFrequentlyTypedWords(searchString)
|
searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule)
|
||||||
clickOnSearchItemInSearchList()
|
clickOnSearchItemInSearchList(composeTestRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clickOnSearchItemInSearchList() {
|
private fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) {
|
||||||
testFlakyView({
|
composeTestRule.apply {
|
||||||
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong())
|
waitUntilTimeout()
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.search_list)).perform(
|
onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick()
|
||||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
}
|
||||||
0,
|
|
||||||
ViewActions.click()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertArticleLoaded() {
|
fun assertArticleLoaded() {
|
||||||
|
@ -21,12 +21,15 @@ package org.kiwix.kiwixmobile.custom.testutils
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||||
import androidx.test.uiautomator.By
|
import androidx.test.uiautomator.By
|
||||||
import androidx.test.uiautomator.UiDevice
|
import androidx.test.uiautomator.UiDevice
|
||||||
import androidx.test.uiautomator.UiObject
|
import androidx.test.uiautomator.UiObject
|
||||||
import androidx.test.uiautomator.UiSelector
|
import androidx.test.uiautomator.UiSelector
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
|
||||||
object TestUtils {
|
object TestUtils {
|
||||||
private const val TAG = "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
|
@JvmStatic
|
||||||
fun deleteTemporaryFilesOfTestCases(context: Context) {
|
fun deleteTemporaryFilesOfTestCases(context: Context) {
|
||||||
context.getExternalFilesDirs(null).filterNotNull()
|
context.getExternalFilesDirs(null).filterNotNull()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user