kopia lustrzana https://github.com/vitorpamplona/amethyst
259 wiersze
10 KiB
Kotlin
259 wiersze
10 KiB
Kotlin
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
|
|
|
import androidx.compose.foundation.Image
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.foundation.text.ClickableText
|
|
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.outlined.Visibility
|
|
import androidx.compose.material.icons.outlined.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.AutofillNode
|
|
import androidx.compose.ui.autofill.AutofillType
|
|
import androidx.compose.ui.focus.onFocusChanged
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.layout.ContentScale
|
|
import androidx.compose.ui.layout.boundsInWindow
|
|
import androidx.compose.ui.layout.onGloballyPositioned
|
|
import androidx.compose.ui.platform.LocalAutofill
|
|
import androidx.compose.ui.platform.LocalAutofillTree
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.platform.LocalUriHandler
|
|
import androidx.compose.ui.res.painterResource
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.*
|
|
import androidx.compose.ui.text.input.*
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.text.style.TextDecoration
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import com.vitorpamplona.amethyst.R
|
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
|
import java.util.*
|
|
|
|
@OptIn(ExperimentalComposeUiApi::class)
|
|
@Composable
|
|
fun LoginPage(
|
|
accountViewModel: AccountStateViewModel,
|
|
isFirstLogin: Boolean
|
|
) {
|
|
val key = remember { mutableStateOf(TextFieldValue("")) }
|
|
var errorMessage by remember { mutableStateOf("") }
|
|
val acceptedTerms = remember { mutableStateOf(!isFirstLogin) }
|
|
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
|
val uri = LocalUriHandler.current
|
|
val context = LocalContext.current
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.verticalScroll(rememberScrollState()),
|
|
verticalArrangement = Arrangement.SpaceBetween
|
|
) {
|
|
// The first child is glued to the top.
|
|
// Hence we have nothing at the top, an empty box is used.
|
|
Box(modifier = Modifier.height(0.dp))
|
|
|
|
// The second child, this column, is centered vertically.
|
|
Column(
|
|
modifier = Modifier
|
|
.padding(20.dp)
|
|
.fillMaxSize(),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
Image(
|
|
painterResource(id = R.drawable.amethyst),
|
|
contentDescription = stringResource(R.string.app_logo),
|
|
modifier = Modifier.size(200.dp),
|
|
contentScale = ContentScale.Inside
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(40.dp))
|
|
|
|
var showPassword by remember {
|
|
mutableStateOf(false)
|
|
}
|
|
|
|
val autofillNode = AutofillNode(
|
|
autofillTypes = listOf(AutofillType.Password),
|
|
onFill = { key.value = TextFieldValue(it) }
|
|
)
|
|
val autofill = LocalAutofill.current
|
|
LocalAutofillTree.current += autofillNode
|
|
|
|
OutlinedTextField(
|
|
modifier = Modifier
|
|
.onGloballyPositioned { coordinates ->
|
|
autofillNode.boundingBox = coordinates.boundsInWindow()
|
|
}
|
|
.onFocusChanged { focusState ->
|
|
autofill?.run {
|
|
if (focusState.isFocused) {
|
|
requestAutofillForNode(autofillNode)
|
|
} else {
|
|
cancelAutofillForNode(autofillNode)
|
|
}
|
|
}
|
|
},
|
|
value = key.value,
|
|
onValueChange = { key.value = it },
|
|
keyboardOptions = KeyboardOptions(
|
|
autoCorrect = false,
|
|
keyboardType = KeyboardType.Password,
|
|
imeAction = ImeAction.Go
|
|
),
|
|
placeholder = {
|
|
Text(
|
|
text = stringResource(R.string.nsec_npub_hex_private_key),
|
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
|
)
|
|
},
|
|
trailingIcon = {
|
|
IconButton(onClick = { showPassword = !showPassword }) {
|
|
Icon(
|
|
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
|
contentDescription = if (showPassword) {
|
|
stringResource(R.string.show_password)
|
|
} else {
|
|
stringResource(
|
|
R.string.hide_password
|
|
)
|
|
}
|
|
)
|
|
}
|
|
},
|
|
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
|
keyboardActions = KeyboardActions(
|
|
onGo = {
|
|
try {
|
|
accountViewModel.login(key.value.text)
|
|
} catch (e: Exception) {
|
|
errorMessage = context.getString(R.string.invalid_key)
|
|
}
|
|
}
|
|
)
|
|
)
|
|
if (errorMessage.isNotBlank()) {
|
|
Text(
|
|
text = errorMessage,
|
|
color = MaterialTheme.colors.error,
|
|
style = MaterialTheme.typography.caption
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(20.dp))
|
|
|
|
if (isFirstLogin) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Checkbox(
|
|
checked = acceptedTerms.value,
|
|
onCheckedChange = { acceptedTerms.value = it }
|
|
)
|
|
|
|
val regularText =
|
|
SpanStyle(color = MaterialTheme.colors.onBackground)
|
|
|
|
val clickableTextStyle =
|
|
SpanStyle(color = MaterialTheme.colors.primary)
|
|
|
|
val annotatedTermsString = buildAnnotatedString {
|
|
withStyle(regularText) {
|
|
append(stringResource(R.string.i_accept_the))
|
|
}
|
|
|
|
withStyle(clickableTextStyle) {
|
|
pushStringAnnotation("openTerms", "")
|
|
append(stringResource(R.string.terms_of_use))
|
|
}
|
|
}
|
|
|
|
ClickableText(
|
|
text = annotatedTermsString
|
|
) { spanOffset ->
|
|
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset)
|
|
.firstOrNull()
|
|
?.also { span ->
|
|
if (span.tag == "openTerms") {
|
|
runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (termsAcceptanceIsRequired.isNotBlank()) {
|
|
Text(
|
|
text = termsAcceptanceIsRequired,
|
|
color = MaterialTheme.colors.error,
|
|
style = MaterialTheme.typography.caption
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(20.dp))
|
|
|
|
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
|
Button(
|
|
onClick = {
|
|
if (!acceptedTerms.value) {
|
|
termsAcceptanceIsRequired =
|
|
context.getString(R.string.acceptance_of_terms_is_required)
|
|
}
|
|
|
|
if (key.value.text.isBlank()) {
|
|
errorMessage = context.getString(R.string.key_is_required)
|
|
}
|
|
|
|
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
|
try {
|
|
accountViewModel.login(key.value.text)
|
|
} catch (e: Exception) {
|
|
errorMessage = context.getString(R.string.invalid_key)
|
|
}
|
|
}
|
|
},
|
|
shape = RoundedCornerShape(35.dp),
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(50.dp),
|
|
colors = ButtonDefaults
|
|
.buttonColors(
|
|
backgroundColor = if (acceptedTerms.value) MaterialTheme.colors.primary else Color.Gray
|
|
)
|
|
) {
|
|
Text(text = stringResource(R.string.login))
|
|
}
|
|
}
|
|
}
|
|
|
|
// The last child is glued to the bottom.
|
|
ClickableText(
|
|
text = AnnotatedString(stringResource(R.string.generate_a_new_key)),
|
|
modifier = Modifier
|
|
.padding(20.dp)
|
|
.fillMaxWidth(),
|
|
onClick = {
|
|
if (acceptedTerms.value) {
|
|
accountViewModel.newKey()
|
|
} else {
|
|
termsAcceptanceIsRequired =
|
|
context.getString(R.string.acceptance_of_terms_is_required)
|
|
}
|
|
},
|
|
style = TextStyle(
|
|
fontSize = 14.sp,
|
|
textDecoration = TextDecoration.Underline,
|
|
color = MaterialTheme.colors.primary,
|
|
textAlign = TextAlign.Center
|
|
)
|
|
)
|
|
}
|
|
}
|