diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31cb424..0fa1874 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,7 +16,7 @@ android { minSdk = 26 targetSdk = 35 versionCode = 1 - versionName = "0.0.8" + versionName = "0.0.9" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ac74ba..055d5fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,22 +19,11 @@ - - - - - - - { _status.value = AuthStatus.Authenticated(authManager.getUsername()) syncManager.sync() } is LoginResult.Failure -> { - _status.value = AuthStatus.Error(result.message) + _status.value = AuthStatus.LoginError(result.message) } } } @@ -76,8 +71,4 @@ class AuthViewModel @Inject constructor( fun clearError() { _status.value = AuthStatus.NotAuthenticated } - - fun handleOAuthError(message: String) { - _status.value = AuthStatus.Error(message) - } } diff --git a/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt b/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt index bcb8046..353c66a 100644 --- a/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt +++ b/app/src/main/java/com/planify/mobile/ui/auth/LoginScreen.kt @@ -1,7 +1,5 @@ package com.planify.mobile.ui.auth -import android.net.Uri -import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -10,50 +8,81 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel import com.planify.mobile.R private val MintBackground = Color(0xFFF0FDF4) private val BonsaiGreen = Color(0xFF38A169) +private val FieldBorder = Color(0xFFD1D5DB) +private val LabelColor = Color(0xFF374151) +private val TitleColor = Color(0xFF111827) @Composable -fun LoginScreen(viewModel: AuthViewModel = hiltViewModel()) { +fun LoginScreen(viewModel: AuthViewModel) { val status by viewModel.status.collectAsState() - val context = LocalContext.current + val isLoading = status is AuthStatus.Checking + val errorMessage = (status as? AuthStatus.LoginError)?.message + + var username by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } + val passwordFocus = remember { FocusRequester() } Box( modifier = Modifier .fillMaxSize() - .background(MintBackground), + .background(MintBackground) + .imePadding(), contentAlignment = Alignment.Center, ) { Surface( shape = RoundedCornerShape(16.dp), - shadowElevation = 4.dp, + shadowElevation = 6.dp, color = Color.White, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 32.dp), + .padding(horizontal = 28.dp) + .verticalScroll(rememberScrollState()), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -65,13 +94,13 @@ fun LoginScreen(viewModel: AuthViewModel = hiltViewModel()) { modifier = Modifier.size(72.dp), ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(10.dp)) Text( text = "Bonsai", fontWeight = FontWeight.Bold, fontSize = 22.sp, - color = Color(0xFF111827), + color = TitleColor, ) Spacer(Modifier.height(4.dp)) @@ -80,57 +109,126 @@ fun LoginScreen(viewModel: AuthViewModel = hiltViewModel()) { text = "Connexion", fontWeight = FontWeight.SemiBold, fontSize = 18.sp, - color = Color(0xFF374151), + color = LabelColor, ) Spacer(Modifier.height(24.dp)) - when (status) { - is AuthStatus.Checking -> { - CircularProgressIndicator(color = BonsaiGreen) - Spacer(Modifier.height(8.dp)) - Text( - text = "Vérification…", - style = MaterialTheme.typography.bodySmall, - color = Color(0xFF6B7280), - ) - } + // Username field + Text( + text = "Email ou nom d'utilisateur", + style = MaterialTheme.typography.bodySmall, + color = LabelColor, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + ) + OutlinedTextField( + value = username, + onValueChange = { username = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !isLoading, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next, + ), + keyboardActions = KeyboardActions( + onNext = { passwordFocus.requestFocus() } + ), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = FieldBorder, + focusedBorderColor = BonsaiGreen, + ), + shape = RoundedCornerShape(6.dp), + ) - is AuthStatus.Error -> { - Text( - text = (status as AuthStatus.Error).message, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(bottom = 12.dp), - ) - LoginButton { - val url = viewModel.buildAuthUrl() - CustomTabsIntent.Builder().setShowTitle(true).build() - .launchUrl(context, Uri.parse(url)) - } - } + Spacer(Modifier.height(12.dp)) - else -> { - LoginButton { - val url = viewModel.buildAuthUrl() - CustomTabsIntent.Builder().setShowTitle(true).build() - .launchUrl(context, Uri.parse(url)) + // Password field + Text( + text = "Mot de passe", + style = MaterialTheme.typography.bodySmall, + color = LabelColor, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp), + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(passwordFocus), + singleLine = true, + enabled = !isLoading, + visualTransformation = if (passwordVisible) VisualTransformation.None + else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { + if (username.isNotBlank() && password.isNotBlank()) { + viewModel.login(username.trim(), password) + } } + ), + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) Icons.Outlined.Visibility + else Icons.Outlined.VisibilityOff, + contentDescription = if (passwordVisible) "Masquer" else "Afficher", + tint = Color(0xFF6B7280), + ) + } + }, + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = FieldBorder, + focusedBorderColor = BonsaiGreen, + ), + shape = RoundedCornerShape(6.dp), + ) + + errorMessage?.let { + Spacer(Modifier.height(8.dp)) + Text( + text = it, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(Modifier.height(20.dp)) + + Button( + onClick = { viewModel.login(username.trim(), password) }, + enabled = !isLoading && username.isNotBlank() && password.isNotBlank(), + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + colors = ButtonDefaults.buttonColors(containerColor = BonsaiGreen), + shape = RoundedCornerShape(6.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = Color.White, + strokeWidth = 2.dp, + ) + } else { + Text( + text = "Se connecter", + color = Color.White, + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + ) } } } } } } - -@Composable -private fun LoginButton(onClick: () -> Unit) { - Button( - onClick = onClick, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors(containerColor = BonsaiGreen), - shape = RoundedCornerShape(8.dp), - ) { - Text("Se connecter", color = Color.White, fontWeight = FontWeight.Medium) - } -}