feat: language fragment to jetpack compose

- simplified app search bar
- renamed the search bar to KiwixSearchView
- added text clearing icon within the textview to
simplify icon logic in app bar
This commit is contained in:
jaskaran 2025-03-22 16:50:28 +05:30 committed by MohitMaliFtechiz
parent 695446e291
commit 1fc4e505a2
6 changed files with 142 additions and 225 deletions

View File

@ -78,17 +78,15 @@ class LanguageFragment : BaseFragment() {
LanguageScreen(
searchText = searchText,
isSearchActive = isSearchActive,
appBarTextFieldTestTag = SEARCH_FIELD_TESTING_TAG,
languageViewModel = languageViewModel,
actionMenuItemList = appBarActionMenuList(
searchText = searchText,
isSearchActive = isSearchActive,
onSearchClick = { isSearchActive = true },
onClearClick = { resetSearchState() },
onSaveClick = {
languageViewModel.actions.offer(Action.SaveAll)
}
),
onClearClick = { resetSearchState() },
onAppBarValueChange = {
searchText = it
languageViewModel.actions.offer(Action.Filter(it))
@ -130,10 +128,8 @@ class LanguageFragment : BaseFragment() {
}
fun appBarActionMenuList(
searchText: String,
isSearchActive: Boolean,
onSearchClick: () -> Unit,
onClearClick: () -> Unit,
onSaveClick: () -> Unit
): List<ActionMenuItem> {
return listOfNotNull(
@ -145,13 +141,6 @@ class LanguageFragment : BaseFragment() {
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

View File

@ -28,19 +28,17 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
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.snapshotFlow
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.AppBarTextField
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
@ -54,8 +52,8 @@ fun LanguageScreen(
isSearchActive: Boolean,
languageViewModel: LanguageViewModel,
actionMenuItemList: List<ActionMenuItem>,
onClearClick: () -> Unit,
onAppBarValueChange: (String) -> Unit,
appBarTextFieldTestTag: String,
content: @Composable() () -> Unit,
) {
val state by languageViewModel.state.observeAsState(State.Loading)
@ -68,11 +66,13 @@ fun LanguageScreen(
navigationIcon = content,
actionMenuItems = actionMenuItemList,
searchBar = if (isSearchActive) {
{
AppBarTextField(
{ modifier ->
KiwixSearchView(
modifier = modifier,
value = searchText,
testTag = appBarTextFieldTestTag,
onValueChange = onAppBarValueChange
testTag = SEARCH_FIELD_TESTING_TAG,
onValueChange = onAppBarValueChange,
onClearClick = onClearClick
)
}
} else {
@ -81,7 +81,8 @@ fun LanguageScreen(
)
}) { innerPadding ->
Column(
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxSize()
// setting bottom padding to zero to avoid accounting for Bottom bar
.padding(
top = innerPadding.calculateTopPadding(),
@ -96,21 +97,10 @@ fun LanguageScreen(
}
is Content -> {
val viewItem = (state as Content).viewItems
LaunchedEffect(viewItem) {
snapshotFlow(listState::firstVisibleItemIndex)
.collect {
if (listState.firstVisibleItemIndex == 2) {
listState.animateScrollToItem(0)
}
}
}
LanguageList(
state = state,
context = context,
listState = listState,
viewItem = viewItem,
selectLanguageItem = { languageItem ->
languageViewModel.actions.offer(Action.Select(languageItem))
}

View File

@ -26,6 +26,8 @@ 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
@ -33,14 +35,26 @@ 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
import org.kiwix.kiwixmobile.language.viewmodel.State
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
@Composable
fun LanguageList(
state: State,
context: Context,
listState: LazyListState,
viewItem: List<LanguageListItem>,
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
) {

View File

@ -1,115 +0,0 @@
/*
* 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.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.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.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.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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBarTextField(
value: String,
testTag: String = "",
onValueChange: (String) -> Unit
) {
val interactionSource = remember(::MutableInteractionSource)
val textStyle = LocalTextStyle.current
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)
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)
.focusRequester(focusRequester),
value = textFieldValue,
onValueChange = {
textFieldValue = it
onValueChange(it.text.replace("\n", ""))
},
textStyle = textStyle.copy(color = Color.White),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
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,
placeholder = {
Text(
text = stringResource(R.string.search_label),
color = Color.LightGray
)
},
colors = colors,
contentPadding = PaddingValues(bottom = 4.dp),
)
}
)
}
}

View File

@ -19,86 +19,64 @@
package org.kiwix.kiwixmobile.core.ui.components
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
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.foundation.lazy.LazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import androidx.compose.ui.text.font.FontWeight.Companion.SemiBold
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray350
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.KIWIX_APP_BAR_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
const val TOOLBAR_TITLE_TESTING_TAG = "toolbarTitle"
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KiwixAppBar(
@StringRes titleId: Int,
navigationIcon: @Composable () -> Unit,
actionMenuItems: List<ActionMenuItem> = emptyList(),
topAppBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
// Optional search bar, used in fragments that require it
searchBar: (@Composable () -> Unit)? = null
searchBar: (@Composable (Modifier) -> Unit)? = null
) {
KiwixTheme {
TopAppBar(
title = { AppBarTitleSection(titleId, searchBar) },
navigationIcon = navigationIcon,
actions = { ActionMenu(actionMenuItems) },
scrollBehavior = topAppBarScrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Black,
scrolledContainerColor = Black
)
)
}
}
@Suppress("ComposableLambdaParameterNaming")
@Composable
private fun AppBarTitleSection(
@StringRes titleId: Int,
searchBar: (@Composable () -> Unit)? = null
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = SIXTEEN_DP),
contentAlignment = Alignment.CenterStart
) {
searchBar?.let {
it()
} ?: run {
AppBarTitle(titleId)
Row(
modifier = Modifier
.fillMaxWidth()
.height(KIWIX_APP_BAR_HEIGHT)
.background(color = Black),
verticalAlignment = Alignment.CenterVertically
) {
navigationIcon()
searchBar?.let { searchBarComposable ->
searchBarComposable(
Modifier.weight(1f)
)
} ?: run {
// Otherwise, show the title
AppBarTitle(titleId)
Spacer(Modifier.weight(1f))
}
ActionMenu(actionMenuItems)
}
}
}
@ -115,11 +93,10 @@ private fun AppBarTitle(
Text(
text = stringResource(titleId),
color = appBarTitleColor,
style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.titleLarge.copy(fontWeight = SemiBold),
modifier = Modifier
.testTag(TOOLBAR_TITLE_TESTING_TAG),
.padding(horizontal = SIXTEEN_DP)
.testTag(TOOLBAR_TITLE_TESTING_TAG)
)
}
@ -130,10 +107,14 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
IconButton(
enabled = menuItem.isEnabled,
onClick = menuItem.onClick,
modifier = Modifier.testTag(menuItem.testingTag)
modifier = Modifier
.testTag(menuItem.testingTag)
) {
Icon(
painter = menuItem.icon.toPainter(),
painter = when (val icon = menuItem.icon) {
is IconItem.Vector -> rememberVectorPainter(icon.imageVector)
is IconItem.Drawable -> painterResource(icon.drawableRes)
},
contentDescription = stringResource(menuItem.contentDescription),
tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray
)
@ -141,23 +122,3 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
}
}
}
@Composable
fun rememberBottomNavigationVisibility(lazyListState: LazyListState?): Boolean {
var isToolbarVisible by remember { mutableStateOf(true) }
var lastScrollIndex by remember { mutableIntStateOf(ZERO) }
val updatedLazyListState = rememberUpdatedState(lazyListState)
LaunchedEffect(updatedLazyListState) {
updatedLazyListState.value?.let { state ->
snapshotFlow { state.firstVisibleItemIndex }
.collect { newScrollIndex ->
if (newScrollIndex != lastScrollIndex) {
isToolbarVisible = newScrollIndex < lastScrollIndex
lastScrollIndex = newScrollIndex
}
}
}
}
return isToolbarVisible
}

View File

@ -0,0 +1,78 @@
/*
* 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
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 org.kiwix.kiwixmobile.core.R
@OptIn(ExperimentalMaterial3Api::class)
@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)
.focusRequester(focusRequester),
singleLine = true,
value = value,
colors = colors,
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 = null
)
}
}
}
)
}