diff --git a/presentation/src/main/java/com/nto/presentation/composable/InputField.kt b/presentation/src/main/java/com/nto/presentation/composable/InputField.kt new file mode 100644 index 0000000..2771dcd --- /dev/null +++ b/presentation/src/main/java/com/nto/presentation/composable/InputField.kt @@ -0,0 +1,195 @@ +package com.nto.presentation.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nto.presentation.R +import com.nto.presentation.theme.BoxGray +import com.nto.presentation.theme.NTOTheme +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Configuration for an [InputField]. + * + * @param containerColor base background color. Overrides background color of textFieldColor and being used inside row's modifier. + * @param textFieldColors textFieldDefaults that are used for textField. Container and indicator colors are being overridden. + * @param paddingValues padding for text. Recommended value is 20.dp from start. + * + **/ + +@Immutable +class InputFieldOptions( + val containerColor: Color = BoxGray, + val textFieldColors: TextFieldColors? = null, + val paddingValues: PaddingValues = PaddingValues(start = 20.dp), + val isConfidential: Boolean = false +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is InputFieldOptions) return false + + if (containerColor != other.containerColor) return false + if (textFieldColors != other.textFieldColors) return false + if (paddingValues != other.paddingValues) return false + + return true + } + + override fun hashCode(): Int { + var result = containerColor.hashCode() + result = 31 * result + textFieldColors.hashCode() + result = 31 * result + paddingValues.hashCode() + return result + } +} + +/** + * High level element that uses Row with TextField to slightly improve final appearance. + * + * @param value mutable variable that represents current text. Should be in viewmodel or screen state. + * @param options instance of [InputFieldOptions] that contains background color, [PaddingValues] and [TextFieldColors] of element. + * @param modifier modifier that should contain [Modifier.height] and [Modifier.width] or other size definition to work correctly. + * @param shape row will be clipped to that shape. + * @param placeholder placeholder text to appear when no user input presents. + * @param onValueChange should contain an update function for [value]. + * + * @sample InputFieldSample + * + */ + +@Composable +fun InputField( + value: String, + modifier: Modifier = Modifier, + options: InputFieldOptions = InputFieldOptions(containerColor = NTOTheme.colors.inputFieldBackground), + shape: Shape = RoundedCornerShape(10.dp), + placeholder: String = "", + onValueChange: (String) -> Unit +) { + val containerColor = options.containerColor + val state = if (!options.isConfidential) null else { + remember { mutableStateOf(true) } + } + Row( + modifier = modifier.then( + Modifier + .clip(shape) + .background(containerColor) + ), verticalAlignment = Alignment.CenterVertically + ) { + TextField(value = value, + modifier = Modifier + .padding(options.paddingValues) + .fillMaxWidth(), + colors = (options.textFieldColors ?: TextFieldDefaults.colors()).copy( + unfocusedContainerColor = containerColor, + unfocusedIndicatorColor = Color.Transparent, + focusedContainerColor = containerColor, + focusedIndicatorColor = Color.Transparent, + ), + onValueChange = onValueChange, + textStyle = NTOTheme.typography.displaySmall, + placeholder = { + Text( + placeholder, + style = NTOTheme.typography.displaySmall, + color = NTOTheme.colors.disabledText, + fontWeight = FontWeight.Bold, + fontSize = 14.sp + ) + }, + trailingIcon = if (options.isConfidential) { + @Composable { + IconButton(modifier = Modifier.size(24.dp), onClick = { + state!!.value = !state.value + }) { + Icon( + painter = painterResource(if (!state!!.value) R.drawable.eye_invisible else R.drawable.eye_visible), + tint = if (value.isBlank()) NTOTheme.colors.disabledText else NTOTheme.colors.secondaryBackground, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } + } else null, + visualTransformation = if (!options.isConfidential) VisualTransformation.None + else if (state!!.value) PasswordVisualTransformation() + else VisualTransformation.None) + } + +} + +@Stable +internal class InputFieldSample { + + class SampleViewModel { + private val _state = MutableStateFlow(SampleState()) + + val state: StateFlow + get() = _state.asStateFlow() + + fun setText(data: String) { + _state.value = _state.value.copy(data = data) + } + } + + data class SampleState( + var data: String = "" + ) + + @Preview + @Composable + private fun InputFieldPreview() { + NTOTheme { + val sampleViewModel = SampleViewModel() + Surface(color = Color.White, modifier = Modifier.fillMaxSize()) { + Column { + InputField( + value = sampleViewModel.state.collectAsState().value.data, + modifier = Modifier + .height(60.dp) + .fillMaxWidth(), + placeholder = stringResource(R.string.placholder_email), + onValueChange = sampleViewModel::setText, + options = InputFieldOptions(isConfidential = true) + ) + } + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginScreen.kt b/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginScreen.kt index ee56430..8d08cc8 100644 --- a/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginScreen.kt +++ b/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginScreen.kt @@ -29,6 +29,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.nto.presentation.R +import com.nto.presentation.composable.InputField +import com.nto.presentation.composable.InputFieldOptions import com.nto.presentation.theme.NTOTheme @Composable @@ -37,7 +39,7 @@ fun LoginScreen( modifier: Modifier = Modifier, viewModel: LoginViewModel = hiltViewModel(), ) { - val state = viewModel.state.collectAsState() + val state = viewModel.state.collectAsState().value Column( modifier = modifier.background(NTOTheme.colors.secondaryBackground), @@ -58,14 +60,14 @@ fun LoginScreen( ) } Text( - text = "Вход", + text = stringResource(R.string.greeting_login), style = NTOTheme.typography.titleLarge, color = NTOTheme.colors.secondaryText, textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(5.dp)) Text( - text = stringResource(R.string.greeting_login), + text = stringResource(R.string.greeting_login_description), style = NTOTheme.typography.displaySmall, color = NTOTheme.colors.secondaryText, textAlign = TextAlign.Center @@ -89,7 +91,11 @@ fun LoginScreen( fontSize = 14.sp ) Spacer(modifier = Modifier.height(5.dp)) - //TODO: InputField + InputField( + state.email, + placeholder = stringResource(R.string.placholder_email), + onValueChange = viewModel::setEmail + ) } Spacer(modifier = Modifier.height(20.dp)) Column(modifier = Modifier.fillMaxWidth()) { @@ -100,7 +106,12 @@ fun LoginScreen( fontSize = 14.sp ) Spacer(modifier = Modifier.height(5.dp)) - //TODO: InputField + InputField( + state.password, + placeholder = stringResource(R.string.placeholder_password), + options = InputFieldOptions(isConfidential = true), + onValueChange = viewModel::setPassword + ) } Spacer(Modifier.height(50.dp)) //TODO: LoginButton diff --git a/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginViewModel.kt b/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginViewModel.kt index 894cde1..275adf8 100644 --- a/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginViewModel.kt +++ b/presentation/src/main/java/com/nto/presentation/screens/loginScreen/LoginViewModel.kt @@ -1,10 +1,14 @@ package com.nto.presentation.screens.loginScreen import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavHostController import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -13,4 +17,24 @@ class LoginViewModel @Inject constructor(): ViewModel(){ val state: StateFlow get() = _state.asStateFlow() + + fun setEmail(value: String) { + _state.tryEmit(_state.value.copy(email = value)) + checkInput() + } + + fun setPassword(value: String) { + _state.tryEmit(_state.value.copy(password = value)) + checkInput() + } + + private fun checkInput() { + //TODO: domain level + } + + fun login(navController: NavHostController) { + viewModelScope.launch(Dispatchers.IO) { + //TODO: domain level + } + } } \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index b720adb..340bfa2 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -3,5 +3,8 @@ Войти Почта Пароль - Войдите в свой аккаунт чтобы продолжить + Войдите в свой аккаунт чтобы продолжить + Вход + example@mail.com + ********** \ No newline at end of file