mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
Merge pull request #4267 from jackq97/#4160-migrate-language-fragment-to-compose
Feat: Language fragment to jetpack compose screen
This commit is contained in:
commit
4236be8d40
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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>
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user