From 1fc4e505a284fe6d89f11bc71c64d6a21ec4ff05 Mon Sep 17 00:00:00 2001 From: jaskaran Date: Sat, 22 Mar 2025 16:50:28 +0530 Subject: [PATCH] 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 --- .../kiwixmobile/language/LanguageFragment.kt | 13 +- .../kiwixmobile/language/LanguageScreen.kt | 32 ++--- .../language/composables/LanguageList.kt | 16 ++- .../core/ui/components/AppBarTextField.kt | 115 ------------------ .../core/ui/components/KiwixAppBar.kt | 113 ++++++----------- .../core/ui/components/KiwixSearchView.kt | 78 ++++++++++++ 6 files changed, 142 insertions(+), 225 deletions(-) delete mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/AppBarTextField.kt create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt index 01c9a0ebd..eb0ecdb1b 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt @@ -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 { 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 diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt index f4dc4322e..cffc65c79 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt @@ -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, + 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)) } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/composables/LanguageList.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/composables/LanguageList.kt index b78139222..ad147832e 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/composables/LanguageList.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/composables/LanguageList.kt @@ -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, 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 ) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/AppBarTextField.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/AppBarTextField.kt deleted file mode 100644 index 44d7d84c2..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/AppBarTextField.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2025 Kiwix - * 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 . - * - */ - -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), - ) - } - ) - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt index 6e7044eae..5ff17898d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt @@ -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 = 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) { 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) { } } } - -@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 -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt new file mode 100644 index 000000000..bbf370f35 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt @@ -0,0 +1,78 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * 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 . + * + */ + +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 + ) + } + } + } + ) +}