mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
Feat: Language fragment to jetpack compose screen
- De-coupled and lifted the states of language screen. - added preview to language screen to better manage the ui. - added tests. - updated the animation logic to Launched effects to avoid circular dependency.
This commit is contained in:
parent
e939ff770d
commit
3e42aa3725
@ -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
|
||||
@ -50,6 +51,9 @@ class LanguageFragmentTest {
|
||||
@JvmField
|
||||
val retryRule = RetryRule()
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private val permissions =
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
@ -61,9 +65,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 +110,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,46 +20,18 @@ 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.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.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.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
|
||||
|
||||
class LanguageFragment : BaseFragment() {
|
||||
@ -67,16 +39,8 @@ class LanguageFragment : BaseFragment() {
|
||||
|
||||
@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 +48,15 @@ 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)
|
||||
|
||||
activity.supportActionBar?.let {
|
||||
it.setDisplayHomeAsUpEnabled(true)
|
||||
it.setHomeAsUpIndicator(drawable.ic_clear_white_24dp)
|
||||
it.setTitle(string.select_languages)
|
||||
composeView.setContent {
|
||||
LanguageScreen(
|
||||
viewModelState = languageViewModel.state,
|
||||
onNavigationClick = activity.onBackPressedDispatcher::onBackPressed,
|
||||
selectLanguageItem = { languageViewModel.actions.offer(Action.Select(it)) },
|
||||
filterText = { languageViewModel.actions.offer(Action.Filter(it)) },
|
||||
saveLanguages = { languageViewModel.actions.offer(Action.SaveAll) },
|
||||
)
|
||||
}
|
||||
// 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))
|
||||
compositeDisposable.add(
|
||||
languageViewModel.effects.subscribe(
|
||||
{
|
||||
@ -116,63 +71,14 @@ class LanguageFragment : BaseFragment() {
|
||||
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,273 @@
|
||||
/*
|
||||
* 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.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
|
||||
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.core.zim_manager.Language
|
||||
import org.kiwix.kiwixmobile.language.composables.AppBarTextField
|
||||
import org.kiwix.kiwixmobile.language.composables.LanguageList
|
||||
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
|
||||
import org.kiwix.kiwixmobile.language.composables.LoadingIndicator
|
||||
import org.kiwix.kiwixmobile.language.viewmodel.State
|
||||
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
|
||||
|
||||
const val SEARCH_ICON_TESTING_TAG = "search"
|
||||
const val SAVE_ICON_TESTING_TAG = "saveLanguages"
|
||||
const val SEARCH_FIELD_TESTING_TAG = "searchField"
|
||||
|
||||
@Suppress("all")
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun LanguageScreen(
|
||||
viewModelState: MutableLiveData<State>,
|
||||
selectLanguageItem: (LanguageItem) -> Unit,
|
||||
filterText: (String) -> Unit,
|
||||
onNavigationClick: () -> Unit,
|
||||
saveLanguages: () -> Unit
|
||||
) {
|
||||
val state by viewModelState.observeAsState(State.Loading)
|
||||
val context = LocalContext.current
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
var updateListState by remember { mutableStateOf(false) }
|
||||
val listState: LazyListState = rememberLazyListState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollToTop = {
|
||||
coroutineScope.launch {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
KiwixTheme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
KiwixAppBar(
|
||||
R.string.select_languages,
|
||||
{
|
||||
NavigationIcon(
|
||||
iconItem = if (isSearchActive) {
|
||||
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack)
|
||||
} else {
|
||||
IconItem.Drawable(
|
||||
R.drawable.ic_close_white_24dp
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (isSearchActive) {
|
||||
isSearchActive = false
|
||||
searchText = ""
|
||||
filterText(searchText)
|
||||
} else {
|
||||
onNavigationClick()
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
listOfNotNull(
|
||||
// First item: conditionally include based on search state
|
||||
when {
|
||||
!isSearchActive -> ActionMenuItem(
|
||||
icon = IconItem.Drawable(R.drawable.action_search),
|
||||
contentDescription = R.string.search_label,
|
||||
onClick = {
|
||||
isSearchActive = true
|
||||
},
|
||||
iconTint = Color.White,
|
||||
isEnabled = true,
|
||||
testingTag = SEARCH_ICON_TESTING_TAG
|
||||
)
|
||||
|
||||
searchText.isNotEmpty() -> ActionMenuItem(
|
||||
icon = IconItem.Drawable(R.drawable.ic_clear_white_24dp),
|
||||
contentDescription = R.string.search_label,
|
||||
onClick = {
|
||||
searchText = ""
|
||||
filterText(searchText)
|
||||
},
|
||||
iconTint = Color.White,
|
||||
isEnabled = true,
|
||||
testingTag = ""
|
||||
)
|
||||
|
||||
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 = {
|
||||
saveLanguages()
|
||||
updateListState = true
|
||||
},
|
||||
iconTint = Color.White,
|
||||
isEnabled = true,
|
||||
testingTag = SAVE_ICON_TESTING_TAG
|
||||
)
|
||||
),
|
||||
searchBar = if (isSearchActive) {
|
||||
{
|
||||
AppBarTextField(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
filterText(it)
|
||||
},
|
||||
testTag = SEARCH_FIELD_TESTING_TAG,
|
||||
hint = stringResource(R.string.search_label),
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions = KeyboardActions.Default
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// spacer to account for top app bar
|
||||
Spacer(modifier = Modifier.height(56.dp))
|
||||
when (state) {
|
||||
State.Loading, State.Saving -> {
|
||||
LoadingIndicator()
|
||||
}
|
||||
|
||||
is Content -> {
|
||||
val viewItem = if (!updateListState) {
|
||||
(state as Content).viewItems
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
LaunchedEffect(viewItem) {
|
||||
snapshotFlow(listState::firstVisibleItemIndex)
|
||||
.collect {
|
||||
if (listState.firstVisibleItemIndex == 2) {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
LanguageList(
|
||||
context = context,
|
||||
listState = listState,
|
||||
viewItem = viewItem,
|
||||
selectLanguageItem = {
|
||||
selectLanguageItem(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LanguageScreenPreview() {
|
||||
val languages = listOf(
|
||||
Language(
|
||||
id = 1,
|
||||
active = true,
|
||||
occurencesOfLanguage = 142,
|
||||
language = "English",
|
||||
languageLocalized = "English",
|
||||
languageCode = "eng",
|
||||
languageCodeISO2 = "en"
|
||||
),
|
||||
Language(
|
||||
id = 2,
|
||||
active = true,
|
||||
occurencesOfLanguage = 86,
|
||||
language = "German",
|
||||
languageLocalized = "Deutsch",
|
||||
languageCode = "deu",
|
||||
languageCodeISO2 = "de"
|
||||
),
|
||||
Language(
|
||||
id = 3,
|
||||
active = true,
|
||||
occurencesOfLanguage = 72,
|
||||
language = "Italian",
|
||||
languageLocalized = "Italiano",
|
||||
languageCode = "ita",
|
||||
languageCodeISO2 = "it"
|
||||
),
|
||||
Language(
|
||||
id = 4,
|
||||
active = true,
|
||||
occurencesOfLanguage = 93,
|
||||
language = "French",
|
||||
languageLocalized = "Français",
|
||||
languageCode = "fra",
|
||||
languageCodeISO2 = "fr"
|
||||
),
|
||||
Language(
|
||||
id = 5,
|
||||
active = true,
|
||||
occurencesOfLanguage = 104,
|
||||
language = "Spanish",
|
||||
languageLocalized = "Español",
|
||||
languageCode = "spa",
|
||||
languageCodeISO2 = "es"
|
||||
)
|
||||
)
|
||||
LanguageScreen(
|
||||
viewModelState = MutableLiveData<State>().apply { value = Content(languages) },
|
||||
selectLanguageItem = {},
|
||||
filterText = {},
|
||||
saveLanguages = {},
|
||||
onNavigationClick = {}
|
||||
)
|
||||
}
|
@ -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,140 @@
|
||||
/*
|
||||
* 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.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Suppress("all")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppBarTextField(
|
||||
value: String,
|
||||
hint: String,
|
||||
testTag: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
val interactionSource = remember(::MutableInteractionSource)
|
||||
val textStyle = LocalTextStyle.current
|
||||
|
||||
val colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
focusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
errorContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
)
|
||||
|
||||
val focusRequester = FocusRequester()
|
||||
SideEffect(focusRequester::requestFocus)
|
||||
|
||||
var textFieldValue by remember {
|
||||
mutableStateOf(TextFieldValue(value, TextRange(value.length)))
|
||||
}
|
||||
textFieldValue = textFieldValue.copy(text = value)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTextSelectionColors provides LocalTextSelectionColors.current
|
||||
) {
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.testTag(testTag)
|
||||
.width(200.dp)
|
||||
.padding(start = 20.dp)
|
||||
.indicatorLine(
|
||||
enabled = true,
|
||||
isError = false,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors
|
||||
)
|
||||
.focusRequester(focusRequester)
|
||||
.semantics {
|
||||
contentDescription = "searchField"
|
||||
},
|
||||
value = textFieldValue,
|
||||
onValueChange = {
|
||||
textFieldValue = it
|
||||
onValueChange(it.text.replace("\n", ""))
|
||||
},
|
||||
textStyle = textStyle.copy(color = Color.White),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
interactionSource = interactionSource,
|
||||
singleLine = true,
|
||||
decorationBox = { innerTextField ->
|
||||
// places text field with placeholder and appropriate bottom padding
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = value,
|
||||
innerTextField = innerTextField,
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
isError = false,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = hint,
|
||||
color = Color.LightGray
|
||||
)
|
||||
},
|
||||
colors = colors,
|
||||
contentPadding = PaddingValues(bottom = 4.dp),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
|
||||
@Composable
|
||||
fun HeaderText(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 = 16.dp, vertical = 8.dp),
|
||||
fontSize = 16.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 androidx.compose.ui.unit.dp
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
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(64.dp)
|
||||
.semantics {
|
||||
contentDescription = context.getString(R.string.select_language_content_description)
|
||||
}
|
||||
.clickable {
|
||||
onCheckedChange(item)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier
|
||||
.padding(16.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(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.HeaderItem
|
||||
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
|
||||
|
||||
@Composable
|
||||
fun LanguageList(
|
||||
context: Context,
|
||||
listState: LazyListState,
|
||||
viewItem: List<LanguageListItem>,
|
||||
selectLanguageItem: (LanguageItem) -> Unit,
|
||||
) {
|
||||
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)
|
||||
is LanguageItem -> LanguageItemRow(
|
||||
context = context,
|
||||
modifier = Modifier
|
||||
.animateItem(
|
||||
fadeInSpec = tween(durationMillis = 250),
|
||||
fadeOutSpec = tween(durationMillis = 100),
|
||||
placementSpec = spring(
|
||||
stiffness = Spring.StiffnessLow,
|
||||
dampingRatio = Spring.DampingRatioLowBouncy
|
||||
)
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.height(64.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
|
||||
|
@ -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
|
||||
@ -15,13 +15,22 @@
|
||||
* 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
|
||||
package org.kiwix.kiwixmobile.language.composables
|
||||
|
||||
class LanguageAdapter(
|
||||
vararg delegates: AdapterDelegate<LanguageListItem>
|
||||
) : BaseDelegateAdapter<LanguageListItem>(*delegates) {
|
||||
override fun getIdFor(item: LanguageListItem) = item.id
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun LoadingIndicator() {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user