mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
feat: language fragment to jetpack compose
- further decoupled the code, shortened the functions. - fixed all detekt long method errors - removed a lot of redundant code
This commit is contained in:
parent
e6271e9977
commit
1cffeb5eac
@ -18,45 +18,121 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.language
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Scaffold
|
||||
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 io.reactivex.disposables.CompositeDisposable
|
||||
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.viewModel
|
||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
||||
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
|
||||
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.composables.AppBarNavigationIcon
|
||||
import org.kiwix.kiwixmobile.language.composables.AppBarTextField
|
||||
import org.kiwix.kiwixmobile.language.viewmodel.Action
|
||||
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
|
||||
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
|
||||
|
||||
val activity = requireActivity() as CoreMainActivity
|
||||
private lateinit var composeView: ComposeView
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
override fun inject(baseActivity: BaseActivity) {
|
||||
baseActivity.cachedComponent.inject(this)
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = requireActivity() as CoreMainActivity
|
||||
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) },
|
||||
)
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
|
||||
fun resetSearchState() {
|
||||
// clears the search text and resets the filter
|
||||
searchText = ""
|
||||
languageViewModel.actions.offer(Action.Filter(searchText))
|
||||
}
|
||||
|
||||
KiwixTheme {
|
||||
Scaffold(topBar = {
|
||||
KiwixAppBar(
|
||||
titleId = R.string.select_languages,
|
||||
navigationIcon = {
|
||||
AppBarNavigationIcon(
|
||||
isSearchActive = isSearchActive,
|
||||
onClick = {
|
||||
if (isSearchActive) {
|
||||
isSearchActive = false
|
||||
resetSearchState()
|
||||
} else {
|
||||
activity.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
actionMenuItems = appBarActionMenuList(
|
||||
searchText = searchText,
|
||||
isSearchActive = isSearchActive,
|
||||
onSearchClick = {
|
||||
isSearchActive = true
|
||||
},
|
||||
onClearClick = {
|
||||
resetSearchState()
|
||||
},
|
||||
onSaveClick = {
|
||||
languageViewModel.actions.offer(Action.SaveAll)
|
||||
}
|
||||
),
|
||||
searchBar = if (isSearchActive) {
|
||||
{
|
||||
AppBarTextField(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
languageViewModel.actions.offer(Action.Filter(it))
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}) {
|
||||
LanguageScreen(
|
||||
languageViewModel = languageViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
compositeAdd(activity)
|
||||
}
|
||||
|
||||
fun compositeAdd(activity: CoreMainActivity) {
|
||||
compositeDisposable.add(
|
||||
languageViewModel.effects.subscribe(
|
||||
{
|
||||
@ -67,6 +143,41 @@ class LanguageFragment : BaseFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
fun appBarActionMenuList(
|
||||
searchText: String,
|
||||
isSearchActive: Boolean,
|
||||
onSearchClick: () -> Unit,
|
||||
onClearClick: () -> 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
|
||||
)
|
||||
|
||||
searchText.isNotEmpty() -> ActionMenuItem(
|
||||
icon = IconItem.Drawable(R.drawable.ic_clear_white_24dp),
|
||||
contentDescription = R.string.search_label,
|
||||
onClick = onClearClick,
|
||||
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 = onSaveClick,
|
||||
testingTag = SAVE_ICON_TESTING_TAG
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -18,182 +18,63 @@
|
||||
|
||||
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.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.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.unit.dp
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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.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.Action
|
||||
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
|
||||
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
|
||||
languageViewModel: LanguageViewModel
|
||||
) {
|
||||
val state by viewModelState.observeAsState(State.Loading)
|
||||
val state by languageViewModel.state.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()
|
||||
|
||||
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()
|
||||
}
|
||||
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 = (state as Content).viewItems
|
||||
|
||||
LaunchedEffect(viewItem) {
|
||||
snapshotFlow(listState::firstVisibleItemIndex)
|
||||
.collect {
|
||||
if (listState.firstVisibleItemIndex == 2) {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
)
|
||||
},
|
||||
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)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
LanguageList(
|
||||
context = context,
|
||||
listState = listState,
|
||||
viewItem = viewItem,
|
||||
selectLanguageItem = { languageItem ->
|
||||
languageViewModel.actions.offer(Action.Select(languageItem))
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
|
||||
import org.kiwix.kiwixmobile.core.ui.models.IconItem
|
||||
|
||||
@Composable
|
||||
fun AppBarNavigationIcon(
|
||||
isSearchActive: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
NavigationIcon(
|
||||
iconItem = if (isSearchActive) {
|
||||
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack)
|
||||
} else {
|
||||
IconItem.Drawable(
|
||||
R.drawable.ic_close_white_24dp
|
||||
)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
@ -29,7 +29,6 @@ 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
|
||||
@ -43,18 +42,18 @@ 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.res.stringResource
|
||||
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
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.language.SEARCH_FIELD_TESTING_TAG
|
||||
|
||||
@Suppress("all")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppBarTextField(
|
||||
value: String,
|
||||
hint: String,
|
||||
testTag: String,
|
||||
onValueChange: (String) -> Unit
|
||||
) {
|
||||
val interactionSource = remember(::MutableInteractionSource)
|
||||
@ -68,29 +67,20 @@ fun AppBarTextField(
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedTextColor = 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)
|
||||
.testTag(SEARCH_FIELD_TESTING_TAG)
|
||||
.width(200.dp)
|
||||
.padding(start = 20.dp)
|
||||
.indicatorLine(
|
||||
enabled = true,
|
||||
isError = false,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors
|
||||
)
|
||||
.focusRequester(focusRequester),
|
||||
value = textFieldValue,
|
||||
onValueChange = {
|
||||
@ -110,10 +100,9 @@ fun AppBarTextField(
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
isError = false,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = hint,
|
||||
text = stringResource(R.string.search_label),
|
||||
color = Color.LightGray
|
||||
)
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user