Change the Authentication Screen.
This commit is contained in:
parent
529ea5f893
commit
b907e934c9
18 changed files with 657 additions and 380 deletions
BIN
app/.DS_Store
vendored
Normal file
BIN
app/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -9,7 +9,7 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdk 33
|
||||
compileSdk 34
|
||||
namespace 'com.pixelized.biblib'
|
||||
|
||||
signingConfigs {
|
||||
|
|
|
|||
20
app/release/output-metadata.json
Normal file
20
app/release/output-metadata.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.pixelized.biblib",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 161,
|
||||
"versionName": "0.1.6",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File"
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
package com.pixelized.biblib.ui.composable
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
|
|
|
|||
|
|
@ -1,32 +1,89 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.SnackbarResult
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.ui.LocalSnackHostState
|
||||
import com.pixelized.biblib.ui.navigation.LocalScreenNavHostController
|
||||
import com.pixelized.biblib.ui.navigation.navigateToHome
|
||||
import com.pixelized.biblib.ui.screen.authentication.composable.CredentialRemember
|
||||
import com.pixelized.biblib.ui.screen.authentication.composable.LoginField
|
||||
import com.pixelized.biblib.ui.screen.authentication.composable.PasswordField
|
||||
import com.pixelized.biblib.ui.screen.authentication.composable.googleStringResource
|
||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationFormViewModel
|
||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.theme.color.ShadowPalette
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import com.pixelized.biblib.utils.extention.modifier.unclickable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
|
|
@ -65,60 +122,287 @@ fun AuthenticationScreen(
|
|||
val snackBarHostState = LocalSnackHostState.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
AuthenticationScreenContent(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
rememberPassword = formViewModel.form.remember,
|
||||
onLoginChange = formViewModel::onLoginChange,
|
||||
onPasswordChange = formViewModel::onPasswordChange,
|
||||
onRememberPasswordChange = formViewModel::onRememberChange,
|
||||
onGoogleSignIn = authenticationViewModel::loginWithGoogle,
|
||||
onSignIn = {
|
||||
authenticationViewModel.login(
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
)
|
||||
},
|
||||
onRegister = {
|
||||
uriHandler.openUri(uri = IBibLibClient.REGISTER_URL)
|
||||
},
|
||||
)
|
||||
val background = rememberSaveable {
|
||||
val tmp = listOf(
|
||||
R.drawable.biblib_background_1,
|
||||
R.drawable.biblib_background_2,
|
||||
R.drawable.biblib_background_3,
|
||||
)
|
||||
mutableStateOf(tmp.random())
|
||||
}
|
||||
val displayBibLibSignIn = remember { mutableStateOf(false) }
|
||||
val blur = remember { derivedStateOf { if (displayBibLibSignIn.value) 8.dp else 0.dp } }
|
||||
|
||||
AuthenticationHandler(
|
||||
onFailure = {
|
||||
scope.launch {
|
||||
val result = snackBarHostState.showSnackbar(
|
||||
message = context.getString(it.message),
|
||||
actionLabel = context.getString(it.action),
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
AuthenticationScreenContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
background = background.value,
|
||||
blur = blur,
|
||||
onGoogleSignIn = authenticationViewModel::loginWithGoogle,
|
||||
onBibLibSingIn = { displayBibLibSignIn.value = true },
|
||||
)
|
||||
|
||||
AuthenticationSignDialog(
|
||||
focusManager = focusManager,
|
||||
visibility = displayBibLibSignIn,
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
rememberPassword = formViewModel.form.remember,
|
||||
onLoginChange = formViewModel::onLoginChange,
|
||||
onPasswordChange = formViewModel::onPasswordChange,
|
||||
onRememberPasswordChange = formViewModel::onRememberChange,
|
||||
onSignIn = {
|
||||
authenticationViewModel.login(
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
when (it) {
|
||||
is AuthenticationErrorUio.GoogleLogin -> {
|
||||
authenticationViewModel.loginWithGoogle()
|
||||
}
|
||||
is AuthenticationErrorUio.Login -> {
|
||||
authenticationViewModel.login(
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
)
|
||||
}
|
||||
},
|
||||
onRegister = {
|
||||
uriHandler.openUri(uri = IBibLibClient.REGISTER_URL)
|
||||
},
|
||||
onDismissRequest = {
|
||||
displayBibLibSignIn.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = displayBibLibSignIn.value
|
||||
) {
|
||||
displayBibLibSignIn.value = false
|
||||
}
|
||||
|
||||
AuthenticationHandler(onFailure = {
|
||||
scope.launch {
|
||||
val result = snackBarHostState.showSnackbar(
|
||||
message = context.getString(it.message),
|
||||
actionLabel = context.getString(it.action),
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
when (it) {
|
||||
is AuthenticationErrorUio.GoogleLogin -> {
|
||||
authenticationViewModel.loginWithGoogle()
|
||||
}
|
||||
|
||||
is AuthenticationErrorUio.Login -> {
|
||||
authenticationViewModel.login(
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = {
|
||||
navHostController.navigateToHome()
|
||||
}
|
||||
)
|
||||
}, onSuccess = {
|
||||
navHostController.navigateToHome()
|
||||
})
|
||||
|
||||
AuthenticationProgress()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthenticationHandler(
|
||||
private fun AuthenticationScreenContent(
|
||||
modifier: Modifier = Modifier,
|
||||
blur: State<Dp>,
|
||||
@DrawableRes background: Int,
|
||||
onGoogleSignIn: () -> Unit,
|
||||
onBibLibSingIn: () -> Unit,
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colors
|
||||
val animatedBlur = animateDpAsState(
|
||||
targetValue = blur.value,
|
||||
// label = "AnimatedContentBlur",
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier.blur(
|
||||
radius = animatedBlur.value,
|
||||
edgeTreatment = BlurredEdgeTreatment.Unbounded,
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(color = MaterialTheme.colors.surface.copy(alpha = 0.8f)),
|
||||
contentScale = ContentScale.FillHeight,
|
||||
painter = painterResource(id = background),
|
||||
contentDescription = null,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(brush = remember {
|
||||
Brush.verticalGradient(
|
||||
listOf(
|
||||
colorScheme.surface.copy(alpha = 0.0f),
|
||||
colorScheme.surface.copy(alpha = 0.0f),
|
||||
colorScheme.surface.copy(alpha = 0.0f),
|
||||
colorScheme.surface.copy(alpha = 0.3f),
|
||||
colorScheme.surface.copy(alpha = 1.0f),
|
||||
)
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.navigationBarsPadding(),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
space = 8.dp, alignment = Alignment.Bottom
|
||||
),
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.height(height = 48.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
onClick = onGoogleSignIn,
|
||||
) {
|
||||
Text(text = googleStringResource())
|
||||
}
|
||||
TextButton(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.height(height = 48.dp),
|
||||
colors = ButtonDefaults.textButtonColors(),
|
||||
onClick = onBibLibSingIn,
|
||||
) {
|
||||
Text(text = "Sign in with BibLib account")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AuthenticationSignDialog(
|
||||
focusManager: FocusManager = LocalFocusManager.current,
|
||||
visibility: State<Boolean>,
|
||||
login: String,
|
||||
onLoginChange: (String) -> Unit,
|
||||
password: String,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
rememberPassword: Boolean,
|
||||
onRememberPasswordChange: (Boolean) -> Unit,
|
||||
onSignIn: () -> Unit,
|
||||
onRegister: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val colorScheme = MaterialTheme.colors
|
||||
val slideOffset = remember { with(density) { 16.dp.roundToPx() } }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.unclickable(disable = visibility.value)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
color = animateColorAsState(
|
||||
targetValue = when (visibility.value) {
|
||||
true -> colorScheme.surface.copy(alpha = 0.3f)
|
||||
else -> Color.Transparent
|
||||
},
|
||||
// label = "AnimatedSignInScrim"
|
||||
).value
|
||||
),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
enter = fadeIn() + slideInVertically { slideOffset },
|
||||
exit = fadeOut() + slideOutVertically { slideOffset },
|
||||
visible = visibility.value,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.align(alignment = Alignment.TopEnd),
|
||||
onClick = onDismissRequest,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.align(alignment = Alignment.Center),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
space = 24.dp,
|
||||
alignment = Alignment.CenterVertically,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
LoginField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = login,
|
||||
onValueChange = onLoginChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
PasswordField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = password,
|
||||
onValueChange = onPasswordChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions { focusManager.clearFocus() },
|
||||
)
|
||||
}
|
||||
CredentialRemember(
|
||||
modifier = Modifier.padding(start = 24.dp, end = 16.dp),
|
||||
value = rememberPassword,
|
||||
onValueChange = onRememberPasswordChange,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = 16.dp, alignment = Alignment.End
|
||||
),
|
||||
) {
|
||||
TextButton(
|
||||
modifier = Modifier.height(height = 48.dp),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colors.onSurface
|
||||
),
|
||||
onClick = onRegister,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.action_register))
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.height(height = 48.dp),
|
||||
colors = ButtonDefaults.buttonColors(),
|
||||
onClick = onSignIn,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.action_login))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AuthenticationHandler(
|
||||
viewModel: AuthenticationViewModel = hiltViewModel(),
|
||||
onFailure: (AuthenticationErrorUio) -> Unit = { },
|
||||
onSuccess: (AuthenticationUio.Done) -> Unit = { },
|
||||
|
|
@ -160,4 +444,42 @@ fun AuthenticationProgress(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun AuthenticationScreenContentPreview(
|
||||
@PreviewParameter(AuthenticationPreviewProvider::class) preview: State<Boolean>,
|
||||
) {
|
||||
BibLibTheme {
|
||||
Surface {
|
||||
AuthenticationScreenContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
blur = remember { mutableStateOf(if (preview.value) 8.dp else 0.dp) },
|
||||
background = R.drawable.biblib_background_3,
|
||||
onGoogleSignIn = { },
|
||||
onBibLibSingIn = { },
|
||||
)
|
||||
AuthenticationSignDialog(
|
||||
visibility = preview,
|
||||
login = "",
|
||||
onLoginChange = { },
|
||||
password = "",
|
||||
onPasswordChange = { },
|
||||
rememberPassword = true,
|
||||
onRememberPasswordChange = { },
|
||||
onSignIn = { },
|
||||
onRegister = { },
|
||||
onDismissRequest = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AuthenticationPreviewProvider : PreviewParameterProvider<State<Boolean>> {
|
||||
override val values: Sequence<State<Boolean>> = sequenceOf(
|
||||
mutableStateOf(false),
|
||||
mutableStateOf(true),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.sharp.Visibility
|
||||
import androidx.compose.material.icons.sharp.VisibilityOff
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
|
||||
import com.pixelized.biblib.utils.extention.autofill
|
||||
|
||||
@Composable
|
||||
fun AuthenticationScreenContent(
|
||||
modifier: Modifier = Modifier,
|
||||
scrollState: ScrollState = rememberScrollState(),
|
||||
login: String,
|
||||
onLoginChange: (String) -> Unit,
|
||||
password: String,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
rememberPassword: Boolean,
|
||||
onRememberPasswordChange: (Boolean) -> Unit,
|
||||
onGoogleSignIn: () -> Unit,
|
||||
onSignIn: () -> Unit,
|
||||
onRegister: () -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
AnimatedDelayer(targetState = "AuthenticationScreenContent") {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier
|
||||
.padding(all = 16.dp)
|
||||
.align(alignment = Alignment.CenterHorizontally),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.h4,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
text = stringResource(id = R.string.authentication_title),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
LoginField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = login,
|
||||
onValueChange = onLoginChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
PasswordField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = password,
|
||||
onValueChange = onPasswordChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions { focusManager.clearFocus() },
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
CredentialRemember(
|
||||
value = rememberPassword,
|
||||
onValueChange = onRememberPasswordChange,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.End),
|
||||
) {
|
||||
Row {
|
||||
Button(
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
onClick = onRegister,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.action_register))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(),
|
||||
onClick = onSignIn,
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.action_login))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
|
||||
AnimatedOffset {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(all = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
onClick = onGoogleSignIn,
|
||||
) {
|
||||
Text(text = googleStringResource())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// region: Content Helper Composable
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun LoginField(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
) {
|
||||
TextField(
|
||||
modifier = modifier.autofill(
|
||||
autofillTypes = listOf(AutofillType.Username, AutofillType.EmailAddress),
|
||||
onFill = onValueChange,
|
||||
),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(text = stringResource(id = R.string.authentication_login)) },
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun PasswordField(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
) {
|
||||
var passwordVisibility by remember { mutableStateOf(false) }
|
||||
TextField(
|
||||
modifier = modifier.autofill(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = onValueChange,
|
||||
),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(text = stringResource(id = R.string.authentication_password)) },
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisibility = passwordVisibility.not() }) {
|
||||
Icon(
|
||||
imageVector = if (passwordVisibility) Icons.Sharp.VisibilityOff else Icons.Sharp.Visibility,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CredentialRemember(
|
||||
modifier: Modifier = Modifier,
|
||||
value: Boolean,
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { onValueChange(value.not()) }
|
||||
)
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
checked = value,
|
||||
onCheckedChange = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
text = stringResource(id = R.string.authentication_credential_remember)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun googleStringResource(): AnnotatedString {
|
||||
val default = LocalTextStyle.current.toSpanStyle()
|
||||
val google = stringResource(id = R.string.action_google_sign_in)
|
||||
return remember {
|
||||
buildAnnotatedString {
|
||||
withStyle(
|
||||
style = default
|
||||
) {
|
||||
append(google)
|
||||
append(" ")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.blue,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("G")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.red,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("o")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.yellow,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("o")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.blue,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("g")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.green,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("l")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.red,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun AuthenticationScreenContentPreview() {
|
||||
BibLibTheme {
|
||||
AuthenticationScreenContent(
|
||||
login = "",
|
||||
onLoginChange = { },
|
||||
password = "",
|
||||
onPasswordChange = { },
|
||||
rememberPassword = true,
|
||||
onRememberPasswordChange = { },
|
||||
onGoogleSignIn = { },
|
||||
onSignIn = { },
|
||||
onRegister = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication.composable
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.biblib.R
|
||||
|
||||
@Composable
|
||||
fun CredentialRemember(
|
||||
modifier: Modifier = Modifier,
|
||||
value: Boolean,
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { onValueChange(value.not()) }
|
||||
)
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
checked = value,
|
||||
onCheckedChange = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
text = stringResource(id = R.string.authentication_credential_remember)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication.composable
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.utils.extention.autofill
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginField(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
) {
|
||||
TextField(
|
||||
modifier = modifier.autofill(
|
||||
autofillTypes = listOf(AutofillType.Username, AutofillType.EmailAddress),
|
||||
onFill = onValueChange,
|
||||
),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(text = stringResource(id = R.string.authentication_login)) },
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication.composable
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.sharp.Visibility
|
||||
import androidx.compose.material.icons.sharp.VisibilityOff
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.utils.extention.autofill
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun PasswordField(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
) {
|
||||
var passwordVisibility by remember { mutableStateOf(false) }
|
||||
TextField(
|
||||
modifier = modifier.autofill(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = onValueChange,
|
||||
),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(text = stringResource(id = R.string.authentication_password)) },
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisibility = passwordVisibility.not() }) {
|
||||
Icon(
|
||||
imageVector = if (passwordVisibility) Icons.Sharp.VisibilityOff else Icons.Sharp.Visibility,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication.composable
|
||||
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
|
||||
|
||||
@Composable
|
||||
fun googleStringResource(): AnnotatedString {
|
||||
val default = LocalTextStyle.current.toSpanStyle()
|
||||
val google = stringResource(id = R.string.action_google_sign_in)
|
||||
return remember {
|
||||
buildAnnotatedString {
|
||||
withStyle(
|
||||
style = default
|
||||
) {
|
||||
append(google)
|
||||
append(" ")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.blue,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("G")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.red,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("o")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.yellow,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("o")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.blue,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("g")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.green,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("l")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.red,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.pixelized.biblib.ui.screen.authentication.viewModel
|
|||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.biblib.BuildConfig
|
||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.ui.screen.authentication.AuthenticationFormUIO
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -56,8 +57,16 @@ class AuthenticationFormViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private val ICredentialRepository.rememberedLogin: String
|
||||
get() = if (rememberCredential) login ?: "" else ""
|
||||
get() = when {
|
||||
rememberCredential -> login ?: ""
|
||||
BuildConfig.DEBUG -> "ThomasTest"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
private val ICredentialRepository.rememberedPassword: String
|
||||
get() = if (rememberCredential) password ?: "" else ""
|
||||
get() = when {
|
||||
rememberCredential -> password ?: ""
|
||||
BuildConfig.DEBUG -> "MotDePasseRandom$2"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.pixelized.biblib.utils
|
||||
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.Stable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
@Stable
|
||||
class DisableInteractionSource : MutableInteractionSource {
|
||||
override val interactions: Flow<Interaction> = flowOf()
|
||||
|
||||
override suspend fun emit(interaction: Interaction) = Unit
|
||||
|
||||
override fun tryEmit(interaction: Interaction): Boolean = false
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.pixelized.biblib.utils.extention.modifier
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.pixelized.biblib.utils.DisableInteractionSource
|
||||
|
||||
@Stable
|
||||
fun Modifier.unclickable(
|
||||
disable: Boolean
|
||||
): Modifier = if (disable) this.clickable(
|
||||
interactionSource = DisableInteractionSource(),
|
||||
indication = null,
|
||||
onClick = { },
|
||||
) else this
|
||||
BIN
app/src/main/res/drawable/biblib_background_1.jpeg
Normal file
BIN
app/src/main/res/drawable/biblib_background_1.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
BIN
app/src/main/res/drawable/biblib_background_2.jpeg
Normal file
BIN
app/src/main/res/drawable/biblib_background_2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 312 KiB |
BIN
app/src/main/res/drawable/biblib_background_3.jpeg
Normal file
BIN
app/src/main/res/drawable/biblib_background_3.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
|
|
@ -37,7 +37,6 @@
|
|||
|
||||
<!-- Screens & pages -->
|
||||
|
||||
<string name="authentication_title">Authentification à BibLib</string>
|
||||
<string name="authentication_login">Alias du compte</string>
|
||||
<string name="authentication_password">Mot de passe</string>
|
||||
<string name="authentication_credential_remember">Mémoriser mes identifiants</string>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@
|
|||
|
||||
<!-- Screens & pages -->
|
||||
|
||||
<string name="authentication_title">Sign in to BibLib</string>
|
||||
<string name="authentication_login">Login</string>
|
||||
<string name="authentication_password">Password</string>
|
||||
<string name="authentication_credential_remember">Remember my credential</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue