Merge pull request #4267 from jackq97/#4160-migrate-language-fragment-to-compose

Feat: Language fragment to jetpack compose screen
This commit is contained in:
Kelson 2025-04-02 18:50:31 +02:00 committed by GitHub
commit 4236be8d40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 691 additions and 450 deletions

View File

@ -19,6 +19,7 @@ package org.kiwix.kiwixmobile.language
import android.Manifest
import android.app.Instrumentation
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.core.content.edit
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
@ -36,6 +37,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER
import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER
import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.testutils.RetryRule
@ -50,6 +52,9 @@ class LanguageFragmentTest {
@JvmField
val retryRule = RetryRule()
@get:Rule(order = COMPOSE_TEST_RULE_ORDER)
val composeTestRule = createComposeRule()
private val permissions =
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
@ -61,9 +66,7 @@ class LanguageFragmentTest {
var permissionRules: GrantPermissionRule =
GrantPermissionRule.grant(*permissions)
private val instrumentation: Instrumentation by lazy {
InstrumentationRegistry.getInstrumentation()
}
private val instrumentation: Instrumentation by lazy(InstrumentationRegistry::getInstrumentation)
init {
AccessibilityChecks.enable().setRunChecksFromRootView(true)
@ -108,45 +111,81 @@ class LanguageFragmentTest {
// search and de-select if german language already selected
clickOnLanguageIcon()
clickOnLanguageSearchIcon()
searchLanguage("german")
deSelectLanguageIfAlreadySelected()
clickOnSaveLanguageIcon()
clickOnLanguageSearchIcon(composeTestRule)
searchLanguage(
composeTestRule = composeTestRule,
searchLanguage = "german"
)
deSelectLanguageIfAlreadySelected(
composeTestRule = composeTestRule,
matchLanguage = "German"
)
clickOnSaveLanguageIcon(composeTestRule)
// search and de-select if italian language already selected
clickOnLanguageIcon()
clickOnLanguageSearchIcon()
searchLanguage("italiano")
deSelectLanguageIfAlreadySelected()
clickOnSaveLanguageIcon()
clickOnLanguageSearchIcon(composeTestRule)
searchLanguage(
composeTestRule = composeTestRule,
searchLanguage = "italiano"
)
deSelectLanguageIfAlreadySelected(
composeTestRule = composeTestRule,
matchLanguage = "Italian"
)
clickOnSaveLanguageIcon(composeTestRule)
// Search and save language for german
clickOnLanguageIcon()
clickOnLanguageSearchIcon()
searchLanguage("german")
selectLanguage("German")
clickOnSaveLanguageIcon()
clickOnLanguageSearchIcon(composeTestRule)
searchLanguage(
composeTestRule = composeTestRule,
searchLanguage = "german"
)
selectLanguage(
composeTestRule = composeTestRule,
matchLanguage = "German"
)
clickOnSaveLanguageIcon(composeTestRule)
// Search and save language for italian
clickOnLanguageIcon()
clickOnLanguageSearchIcon()
searchLanguage("italiano")
selectLanguage("Italian")
clickOnSaveLanguageIcon()
clickOnLanguageSearchIcon(composeTestRule)
searchLanguage(
composeTestRule = composeTestRule,
searchLanguage = "italiano"
)
selectLanguage(
composeTestRule = composeTestRule,
matchLanguage = "Italian"
)
clickOnSaveLanguageIcon(composeTestRule)
// verify is german language selected
clickOnLanguageIcon()
clickOnLanguageSearchIcon()
searchLanguage("german")
checkIsLanguageSelected()
clickOnSaveLanguageIcon()
clickOnLanguageSearchIcon(composeTestRule)
searchLanguage(
composeTestRule = composeTestRule,
searchLanguage = "german"
)
checkIsLanguageSelected(
composeTestRule = composeTestRule,
matchLanguage = "German"
)
clickOnSaveLanguageIcon(composeTestRule)
// verify is italian language selected
clickOnLanguageIcon()
clickOnLanguageSearchIcon()
searchLanguage("italiano")
checkIsLanguageSelected()
clickOnSaveLanguageIcon()
clickOnLanguageSearchIcon(composeTestRule)
searchLanguage(
composeTestRule = composeTestRule,
searchLanguage = "italiano"
)
checkIsLanguageSelected(
composeTestRule = composeTestRule,
matchLanguage = "Italian"
)
clickOnSaveLanguageIcon(composeTestRule)
}
LeakAssertions.assertNoLeaks()
}

View File

@ -18,27 +18,28 @@
package org.kiwix.kiwixmobile.language
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import applyWithViewHierarchyPrinting
import com.adevinta.android.barista.interaction.BaristaSleepInteractions
import com.adevinta.android.barista.interaction.BaristaSwipeRefreshInteractions.refresh
import junit.framework.AssertionFailedError
import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.StringId.TextId
import org.kiwix.kiwixmobile.Findable.Text
import org.kiwix.kiwixmobile.Findable.ViewId
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.language.composables.LANGUAGE_ITEM_CHECKBOX_TESTING_TAG
import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
import org.kiwix.kiwixmobile.utils.RecyclerViewMatcher
fun language(func: LanguageRobot.() -> Unit) = LanguageRobot().applyWithViewHierarchyPrinting(func)
@ -82,57 +83,57 @@ class LanguageRobot : BaseRobot() {
}
fun clickOnLanguageIcon() {
// Wait for a few seconds to properly saved selected language.
// Wait for a few seconds to properly save selected language.
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
clickOn(ViewId(R.id.select_language))
}
fun clickOnLanguageSearchIcon() {
testFlakyView({ onView(withId(R.id.menu_language_search)).perform(click()) })
fun clickOnSaveLanguageIcon(composeTestRule: ComposeContentTestRule) {
composeTestRule.onNodeWithTag(SAVE_ICON_TESTING_TAG)
.performClick()
}
fun searchLanguage(searchLanguage: String) {
isVisible(ViewId(androidx.appcompat.R.id.search_src_text)).text = searchLanguage
fun clickOnLanguageSearchIcon(composeTestRule: ComposeContentTestRule) {
composeTestRule.onNodeWithTag(SEARCH_ICON_TESTING_TAG).performClick()
}
fun selectLanguage(matchLanguage: String) {
testFlakyView({ clickOn(Text(matchLanguage)) })
fun searchLanguage(
composeTestRule: ComposeContentTestRule,
searchLanguage: String
) {
val searchField = composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG)
searchField.performTextInput(text = searchLanguage)
}
fun clickOnSaveLanguageIcon() {
clickOn(ViewId(R.id.menu_language_save))
}
fun checkIsLanguageSelected() {
// Wait for a second to properly visible the searched language on top.
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
onView(
RecyclerViewMatcher(R.id.language_recycler_view).atPositionOnView(
1,
R.id.item_language_checkbox
)
).check(
matches(isChecked())
)
}
fun deSelectLanguageIfAlreadySelected() {
// Wait for a second to properly visible the searched language on top.
// error prone
fun deSelectLanguageIfAlreadySelected(
composeTestRule: ComposeContentTestRule,
matchLanguage: String
) {
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
try {
onView(
RecyclerViewMatcher(R.id.language_recycler_view).atPositionOnView(
1,
R.id.item_language_checkbox
)
).check(matches(isNotChecked()))
} catch (assertionError: AssertionFailedError) {
onView(
RecyclerViewMatcher(R.id.language_recycler_view).atPositionOnView(
1,
R.id.item_language_checkbox
)
).perform(click())
composeTestRule.onNodeWithTag("$LANGUAGE_ITEM_CHECKBOX_TESTING_TAG$matchLanguage")
.assertIsOff()
} catch (noMatchingNodeException: AssertionError) {
composeTestRule.onNodeWithTag("$LANGUAGE_ITEM_CHECKBOX_TESTING_TAG$matchLanguage")
.performClick()
}
}
fun selectLanguage(
composeTestRule: ComposeContentTestRule,
matchLanguage: String
) {
composeTestRule.onNodeWithText(matchLanguage)
.performClick()
}
fun checkIsLanguageSelected(
composeTestRule: ComposeContentTestRule,
matchLanguage: String
) {
BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS.toLong())
composeTestRule.onNodeWithTag("$LANGUAGE_ITEM_CHECKBOX_TESTING_TAG$matchLanguage")
.assertIsOn()
}
}

View File

@ -20,62 +20,42 @@ package org.kiwix.kiwixmobile.language
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.disposables.CompositeDisposable
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.R.drawable
import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.extensions.closeKeyboard
import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
import org.kiwix.kiwixmobile.core.extensions.setUpSearchView
import org.kiwix.kiwixmobile.core.extensions.viewModel
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.utils.SimpleTextListener
import org.kiwix.kiwixmobile.databinding.ActivityLanguageBinding
import org.kiwix.kiwixmobile.language.adapter.LanguageAdapter
import org.kiwix.kiwixmobile.language.adapter.LanguageDelegate.HeaderDelegate
import org.kiwix.kiwixmobile.language.adapter.LanguageDelegate.LanguageItemDelegate
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.language.viewmodel.Action
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter
import org.kiwix.kiwixmobile.language.viewmodel.Action.Select
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
import org.kiwix.kiwixmobile.language.viewmodel.State
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
import org.kiwix.kiwixmobile.language.viewmodel.State.Loading
import org.kiwix.kiwixmobile.language.viewmodel.State.Saving
import javax.inject.Inject
const val SEARCH_ICON_TESTING_TAG = "search"
const val SAVE_ICON_TESTING_TAG = "saveLanguages"
const val SEARCH_FIELD_TESTING_TAG = "searchField"
class LanguageFragment : BaseFragment() {
private val languageViewModel by lazy { viewModel<LanguageViewModel>(viewModelFactory) }
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var composeView: ComposeView
private val compositeDisposable = CompositeDisposable()
private var activityLanguageBinding: ActivityLanguageBinding? = null
private var searchView: SearchView? = null
private val languageAdapter =
LanguageAdapter(
LanguageItemDelegate { languageViewModel.actions.offer(Select(it)) },
HeaderDelegate()
)
override fun inject(baseActivity: BaseActivity) {
baseActivity.cachedComponent.inject(this)
@ -84,24 +64,59 @@ class LanguageFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as CoreMainActivity
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
activity.setSupportActionBar(toolbar)
composeView.setContent {
var searchText by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) }
activity.supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(drawable.ic_clear_white_24dp)
it.setTitle(string.select_languages)
fun resetSearchState() {
// clears the search text and resets the filter
searchText = ""
languageViewModel.actions.offer(Action.Filter(searchText))
}
KiwixTheme {
LanguageScreen(
searchText = searchText,
isSearchActive = isSearchActive,
languageViewModel = languageViewModel,
actionMenuItemList = appBarActionMenuList(
isSearchActive = isSearchActive,
onSearchClick = { isSearchActive = true },
onSaveClick = {
languageViewModel.actions.offer(Action.SaveAll)
}
),
onClearClick = { resetSearchState() },
onAppBarValueChange = {
searchText = it
languageViewModel.actions.offer(Action.Filter(it))
},
navigationIcon = {
NavigationIcon(
iconItem = if (isSearchActive) {
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack)
} else {
IconItem.Drawable(
R.drawable.ic_close_white_24dp
)
},
onClick = {
if (isSearchActive) {
isSearchActive = false
resetSearchState()
} else {
activity.onBackPressedDispatcher.onBackPressed()
}
}
)
}
)
}
}
// set the contentDescription to navigation back button
toolbar.getToolbarNavigationIcon()?.setToolTipWithContentDescription(
getString(string.toolbar_back_button_content_description)
)
activityLanguageBinding?.languageRecyclerView?.run {
adapter = languageAdapter
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
setHasFixedSize(true)
}
languageViewModel.state.observe(viewLifecycleOwner, Observer(::render))
compositeAdd(activity)
}
fun compositeAdd(activity: CoreMainActivity) {
compositeDisposable.add(
languageViewModel.effects.subscribe(
{
@ -112,67 +127,44 @@ class LanguageFragment : BaseFragment() {
)
}
fun appBarActionMenuList(
isSearchActive: Boolean,
onSearchClick: () -> Unit,
onSaveClick: () -> Unit
): List<ActionMenuItem> {
return listOfNotNull(
when {
!isSearchActive -> ActionMenuItem(
icon = IconItem.Drawable(R.drawable.action_search),
contentDescription = R.string.search_label,
onClick = onSearchClick,
testingTag = SEARCH_ICON_TESTING_TAG
)
else -> null // Handle the case when both conditions are false
},
// Second item: always included
ActionMenuItem(
icon = IconItem.Vector(Icons.Default.Check),
contentDescription = R.string.save_languages,
onClick = onSaveClick,
testingTag = SAVE_ICON_TESTING_TAG
)
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setupMenu()
activityLanguageBinding = ActivityLanguageBinding.inflate(inflater, container, false)
return activityLanguageBinding?.root
}
private fun setupMenu() {
(requireActivity() as MenuHost).addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_language, menu)
searchView = menu.findItem(R.id.menu_language_search).actionView as SearchView
searchView?.apply {
setUpSearchView(requireActivity())
setOnQueryTextListener(
SimpleTextListener { query, _ ->
languageViewModel.actions.offer(Filter(query))
}
)
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.menu_language_save -> {
closeKeyboard()
languageViewModel.actions.offer(Action.SaveAll)
true
}
else -> false
}
}
},
viewLifecycleOwner,
Lifecycle.State.RESUMED
)
}
private fun render(state: State) =
when (state) {
Loading -> activityLanguageBinding?.languageProgressbar?.show()
is Content -> {
activityLanguageBinding?.languageProgressbar?.hide()
languageAdapter.items = state.viewItems
}
Saving -> Unit
): View {
return ComposeView(requireContext()).also {
composeView = it
}
}
override fun onDestroyView() {
super.onDestroyView()
compositeDisposable.clear()
activityLanguageBinding?.root?.removeAllViews()
searchView?.setOnQueryTextListener(null)
searchView = null
activityLanguageBinding?.languageRecyclerView?.adapter = null
activityLanguageBinding = null
}
}

View File

@ -0,0 +1,126 @@
/*
* 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.language
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
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.language.composables.LanguageList
import org.kiwix.kiwixmobile.language.viewmodel.Action
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
import org.kiwix.kiwixmobile.language.viewmodel.State
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("ComposableLambdaParameterNaming")
@Composable
fun LanguageScreen(
searchText: String,
isSearchActive: Boolean,
languageViewModel: LanguageViewModel,
actionMenuItemList: List<ActionMenuItem>,
onClearClick: () -> Unit,
onAppBarValueChange: (String) -> Unit,
navigationIcon: @Composable() () -> Unit = {}
) {
val state by languageViewModel.state.observeAsState(State.Loading)
val listState: LazyListState = rememberLazyListState()
val context = LocalContext.current
Scaffold(topBar = {
KiwixAppBar(
titleId = R.string.select_languages,
navigationIcon = navigationIcon,
actionMenuItems = actionMenuItemList,
searchBar = if (isSearchActive) {
{
KiwixSearchView(
value = searchText,
testTag = SEARCH_FIELD_TESTING_TAG,
onValueChange = onAppBarValueChange,
onClearClick = onClearClick,
modifier = Modifier
)
}
} else {
null
}
)
}) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
// setting bottom padding to zero to avoid accounting for Bottom bar
.padding(
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(LocalLayoutDirection.current),
end = innerPadding.calculateEndPadding(LocalLayoutDirection.current),
bottom = 0.dp
)
) {
when (state) {
State.Loading, State.Saving -> {
LoadingScreen()
}
is Content -> {
LanguageList(
state = state,
context = context,
listState = listState,
selectLanguageItem = { languageItem ->
languageViewModel.actions.offer(Action.Select(languageItem))
}
)
}
}
}
}
}
@Composable
fun LoadingScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
ContentLoadingProgressBar()
}
}

View File

@ -1,27 +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.language.adapter
import org.kiwix.kiwixmobile.core.base.adapter.AdapterDelegate
import org.kiwix.kiwixmobile.core.base.adapter.BaseDelegateAdapter
class LanguageAdapter(
vararg delegates: AdapterDelegate<LanguageListItem>
) : BaseDelegateAdapter<LanguageListItem>(*delegates) {
override fun getIdFor(item: LanguageListItem) = item.id
}

View File

@ -1,52 +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.language.adapter
import android.view.ViewGroup
import org.kiwix.kiwixmobile.core.base.adapter.AbsDelegateAdapter
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.viewBinding
import org.kiwix.kiwixmobile.databinding.HeaderDateBinding
import org.kiwix.kiwixmobile.databinding.ItemLanguageBinding
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListViewHolder.HeaderViewHolder
import org.kiwix.kiwixmobile.language.adapter.LanguageListViewHolder.LanguageViewHolder
sealed class LanguageDelegate<I : LanguageListItem, out VH : LanguageListViewHolder<I>> :
AbsDelegateAdapter<I, LanguageListItem, VH> {
class HeaderDelegate : LanguageDelegate<HeaderItem, HeaderViewHolder>() {
override val itemClass = HeaderItem::class.java
override fun createViewHolder(parent: ViewGroup) =
HeaderViewHolder(
parent.viewBinding(HeaderDateBinding::inflate, false)
)
}
class LanguageItemDelegate(private val clickAction: (LanguageItem) -> Unit) :
LanguageDelegate<LanguageItem, LanguageViewHolder>() {
override val itemClass = LanguageItem::class.java
override fun createViewHolder(parent: ViewGroup) =
LanguageViewHolder(
parent.viewBinding(ItemLanguageBinding::inflate, false),
clickAction
)
}
}

View File

@ -1,70 +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.language.adapter
import android.view.View
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
import org.kiwix.kiwixmobile.databinding.HeaderDateBinding
import org.kiwix.kiwixmobile.databinding.ItemLanguageBinding
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
sealed class LanguageListViewHolder<in T : LanguageListItem>(override val containerView: View) :
BaseViewHolder<T>(containerView) {
class HeaderViewHolder(private val headerDateBinding: HeaderDateBinding) :
LanguageListViewHolder<HeaderItem>(headerDateBinding.root) {
override fun bind(item: HeaderItem) {
headerDateBinding.headerDate.setText(
if (item.id == HeaderItem.SELECTED) {
R.string.your_languages
} else {
R.string.other_languages
}
)
}
}
class LanguageViewHolder(
private val itemLanguageBinding: ItemLanguageBinding,
val clickAction: (LanguageItem) -> Unit
) : LanguageListViewHolder<LanguageItem>(itemLanguageBinding.root) {
override fun bind(item: LanguageItem) {
val language = item.language
itemLanguageBinding.itemLanguageName.text = language.language
itemLanguageBinding.itemLanguageLocalizedName.text = language.languageLocalized
itemLanguageBinding.itemLanguageBooksCount.text =
containerView.context
.getString(R.string.books_count, language.occurencesOfLanguage)
itemLanguageBinding.itemLanguageCheckbox.apply {
setToolTipWithContentDescription(
containerView.context.getString(R.string.select_language_content_description)
)
isChecked = language.active
}
itemLanguageBinding.itemLanguageClickableArea.apply {
setToolTipWithContentDescription(
containerView.context.getString(R.string.select_language_content_description)
)
setOnClickListener { clickAction(item) }
}
}
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.language.composables
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
@Composable
fun HeaderText(
modifier: Modifier,
item: LanguageListItem.HeaderItem
) {
Text(
text = when (item.id) {
LanguageListItem.HeaderItem.SELECTED -> stringResource(R.string.your_languages)
LanguageListItem.HeaderItem.OTHER -> stringResource(R.string.other_languages)
else -> ""
},
modifier = modifier
.padding(horizontal = ComposeDimens.SIXTEEN_DP, vertical = ComposeDimens.EIGHT_DP),
fontSize = ComposeDimens.FOURTEEN_SP,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}

View File

@ -0,0 +1,95 @@
/*
* 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.language.composables
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
const val LANGUAGE_ITEM_CHECKBOX_TESTING_TAG = "languageItemCheckboxTestingTag"
@Composable
fun LanguageItemRow(
context: Context,
modifier: Modifier,
item: LanguageItem,
onCheckedChange: (LanguageItem) -> Unit
) {
val language = item.language
Row(
modifier = modifier
.fillMaxWidth()
.height(ComposeDimens.SIXTY_FOUR_DP)
.semantics {
contentDescription = context.getString(R.string.select_language_content_description)
}
.clickable {
onCheckedChange(item)
},
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
modifier = Modifier
.padding(ComposeDimens.SIXTEEN_DP)
.semantics {
testTag = "$LANGUAGE_ITEM_CHECKBOX_TESTING_TAG${language.language}"
},
checked = language.active,
onCheckedChange = {
onCheckedChange(item)
}
)
Column {
Text(
text = language.language,
style = MaterialTheme.typography.bodyLarge
)
Text(
text = language.languageLocalized,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondary
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = stringResource(R.string.books_count, language.occurencesOfLanguage),
modifier = Modifier.padding(ComposeDimens.SIXTEEN_DP),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondary
)
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.language.composables
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.viewmodel.State
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
@Composable
fun LanguageList(
state: State,
context: Context,
listState: LazyListState,
selectLanguageItem: (LanguageItem) -> Unit,
) {
val viewItem = (state as Content).viewItems
LaunchedEffect(viewItem) {
snapshotFlow(listState::firstVisibleItemIndex)
.collect {
if (listState.firstVisibleItemIndex == 2) {
listState.animateScrollToItem(0)
}
}
}
LazyColumn(
state = listState
) {
items(
items = viewItem,
key = { item ->
when (item) {
is HeaderItem -> "header_${item.id}"
is LanguageItem -> "language_${item.language.id}"
}
}
) { item ->
when (item) {
is HeaderItem -> HeaderText(
item = item,
modifier = Modifier
.animateItem()
)
is LanguageItem -> LanguageItemRow(
context = context,
modifier = Modifier
.animateItem()
.fillMaxWidth()
.height(ComposeDimens.SIXTY_FOUR_DP)
.semantics {
contentDescription =
context.getString(R.string.select_language_content_description)
}
.clickable {
selectLanguageItem(item)
},
item = item,
onCheckedChange = { selectLanguageItem(it) }
)
}
}
}
}

View File

@ -1,6 +1,6 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@ -16,7 +16,7 @@
*
*/
package org.kiwix.kiwixmobile.language.adapter
package org.kiwix.kiwixmobile.language.composables
import org.kiwix.kiwixmobile.core.zim_manager.Language

View File

@ -19,7 +19,7 @@
package org.kiwix.kiwixmobile.language.viewmodel
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
sealed class Action {
data class UpdateLanguages(val languages: List<Language>) : Action()

View File

@ -18,13 +18,13 @@
package org.kiwix.kiwixmobile.language.viewmodel
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.processors.PublishProcessor
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter
import org.kiwix.kiwixmobile.language.viewmodel.Action.SaveAll
import org.kiwix.kiwixmobile.language.viewmodel.Action.Select

View File

@ -19,9 +19,9 @@
package org.kiwix.kiwixmobile.language.viewmodel
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
sealed class State {
object Loading : State()

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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="match_parent"
android:layout_height="match_parent">
<include layout="@layout/layout_standard_app_bar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/language_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/pref_language_title"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/app_bar"
android:clipToPadding="false"
tools:listitem="@layout/item_language" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/language_progressbar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/app_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -120,8 +120,7 @@
<fragment
android:id="@+id/languageFragment"
android:name="org.kiwix.kiwixmobile.language.LanguageFragment"
android:label="LanguageFragment"
tools:layout="@layout/activity_language" />
android:label="LanguageFragment" />
<fragment
android:id="@+id/zimHostFragment"
android:name="org.kiwix.kiwixmobile.webserver.ZimHostFragment"

View File

@ -1,76 +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.language.adapter
import android.view.ViewGroup
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.viewBinding
import org.kiwix.kiwixmobile.databinding.HeaderDateBinding
import org.kiwix.kiwixmobile.databinding.ItemLanguageBinding
import org.kiwix.kiwixmobile.language.adapter.LanguageDelegate.HeaderDelegate
import org.kiwix.kiwixmobile.language.adapter.LanguageDelegate.LanguageItemDelegate
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListViewHolder.HeaderViewHolder
import org.kiwix.kiwixmobile.language.adapter.LanguageListViewHolder.LanguageViewHolder
class LanguageDelegateTest {
@Nested
inner class HeaderDelegateTests {
@Test
fun `class is header item`() {
assertThat(HeaderDelegate().itemClass).isEqualTo(HeaderItem::class.java)
}
@Test
fun `creates HeaderViewHolder`() {
val parent = mockk<ViewGroup>()
mockkObject(ViewGroupExtensions)
every { parent.viewBinding(HeaderDateBinding::inflate, false) } returns mockk()
every { parent.viewBinding(HeaderDateBinding::inflate, false).root } returns mockk()
assertThat(HeaderDelegate().createViewHolder(parent))
.isInstanceOf(HeaderViewHolder::class.java)
}
}
@Nested
inner class LanguageItemDelegateTests {
@Test
fun `class is lanuguage item`() {
assertThat(LanguageItemDelegate {}.itemClass).isEqualTo(LanguageItem::class.java)
}
@Test
fun `creates LanguageViewHolder`() {
val parent = mockk<ViewGroup>()
mockkObject(ViewGroupExtensions)
every { parent.viewBinding(ItemLanguageBinding::inflate, false) } returns mockk()
every { parent.viewBinding(ItemLanguageBinding::inflate, false).root } returns mockk()
val clickAction = mockk<(LanguageItem) -> Unit>()
assertThat(LanguageItemDelegate(clickAction).createViewHolder(parent))
.isInstanceOf(LanguageViewHolder::class.java)
}
}
}

View File

@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter
import org.kiwix.kiwixmobile.language.viewmodel.Action.SaveAll
import org.kiwix.kiwixmobile.language.viewmodel.Action.Select

View File

@ -21,8 +21,8 @@ package org.kiwix.kiwixmobile.language.viewmodel
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.HeaderItem
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
import org.kiwix.sharedFunctions.language

View File

@ -22,8 +22,12 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -78,7 +82,11 @@ fun KiwixAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Black,
scrolledContainerColor = Black
)
),
// Edge-to-Edge mode is already enabled in our application,
// so we don't need to apply additional top insets.
// This prevents unwanted extra margin at the top.
windowInsets = WindowInsets.statusBars.only(WindowInsetsSides.Horizontal)
)
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.ui.components
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
@Composable
fun KiwixSearchView(
modifier: Modifier,
value: String,
testTag: String = "",
onValueChange: (String) -> Unit,
onClearClick: () -> Unit
) {
val colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color.White
)
val focusRequester = FocusRequester()
SideEffect(focusRequester::requestFocus)
TextField(
modifier = modifier
.testTag(testTag)
.minimumInteractiveComponentSize()
.focusRequester(focusRequester),
singleLine = true,
value = value,
placeholder = {
Text(
text = stringResource(R.string.search_label),
color = Color.LightGray,
fontSize = ComposeDimens.EIGHTEEN_SP
)
},
colors = colors,
textStyle = TextStyle.Default.copy(
fontSize = ComposeDimens.EIGHTEEN_SP
),
onValueChange = {
onValueChange(it.replace("\n", ""))
},
trailingIcon = {
if (value.isNotEmpty()) {
IconButton(onClick = onClearClick) {
Icon(
painter = painterResource(R.drawable.ic_clear_white_24dp),
tint = Color.White,
contentDescription = stringResource(R.string.searchview_description_clear)
)
}
}
}
)
}

View File

@ -52,10 +52,12 @@ object ComposeDimens {
val FOUR_DP = 4.dp
val TWO_DP = 2.dp
val SEVENTY_DP = 70.dp
val SIXTY_FOUR_DP = 64.dp
// Font Sizes
val TWENTY_FOUR_SP = 24.sp
val FOURTEEN_SP = 14.sp
val EIGHTEEN_SP = 18.sp
// Default letter spacing in text according to theme
val DEFAULT_LETTER_SPACING = 0.0333.em

View File

@ -405,4 +405,5 @@
<string name="resuming_state">Resuming</string>
<string name="downloading_state">Downloading</string>
<string name="download_failed_state">Failed</string>
<string name="searchview_description_clear">Clear query</string>
</resources>