diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 660eb6d5a..73496050d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -5,32 +5,20 @@ import android.util.LruCache import android.util.Patterns import androidx.compose.animation.animateContentSize 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.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.core.os.ConfigurationCompat import androidx.navigation.NavController @@ -48,12 +36,12 @@ import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toNote import com.vitorpamplona.amethyst.service.Nip19 import com.vitorpamplona.amethyst.ui.note.toShortenHex +import nostr.postr.toNpub import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL -import java.util.Locale +import java.util.* import java.util.regex.Pattern -import nostr.postr.toNpub val imageExtension = Pattern.compile("(.*/)*.+\\.(png|jpg|gif|bmp|jpeg|webp|svg)$") val videoExtension = Pattern.compile("(.*/)*.+\\.(mp4|avi|wmv|mpg|amv|webm)$") @@ -195,25 +183,35 @@ fun RichTextViewer(content: String, canPreview: Boolean, tags: List if (source != null && target != null) { if (source != target) { - FlowRow() { - Text( - text = "Auto-translated from ", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - ) - ClickableText( - text = AnnotatedString(Locale(source).displayName), - onClick = { showOriginal = true }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f)) - ) - Text( - text = " to ", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - ) - ClickableText( - text = AnnotatedString(Locale(target).displayName), - onClick = { showOriginal = false }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(alpha = 0.52f)) - ) + val clickableTextStyle = SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f)) + + val annotatedTranslationString= buildAnnotatedString { + append("Auto-translated from ") + + withStyle(clickableTextStyle) { + pushStringAnnotation("showOriginal", true.toString()) + append(Locale(source).displayName) + } + + append(" to ") + + withStyle(clickableTextStyle) { + pushStringAnnotation("showOriginal", false.toString()) + append(Locale(target).displayName) + } + } + + ClickableText( + text = annotatedTranslationString, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), + overflow = TextOverflow.Visible, + maxLines = 3 + ) { spanOffset -> + annotatedTranslationString.getStringAnnotations(spanOffset, spanOffset) + .firstOrNull() + ?.also { span -> + showOriginal = span.item.toBoolean() + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index bc3dbb1a1..c3494d656 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -228,7 +228,7 @@ private fun ProfileHeader( contentPadding = PaddingValues(0.dp) ) { Icon( - tint = if (isSystemInDarkTheme()) Color.White else Color.Black, + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), imageVector = Icons.Default.MoreVert, contentDescription = "More Options", ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index f45f9837a..787f362a4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -1,37 +1,18 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.foundation.Image -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.Spacer -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.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.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Checkbox -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text +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.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -40,16 +21,13 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle -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.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation +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.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.components.sendMail @Composable fun LoginPage(accountViewModel: AccountStateViewModel) { @@ -59,12 +37,144 @@ fun LoginPage(accountViewModel: AccountStateViewModel) { var termsAcceptanceIsRequired by remember { mutableStateOf("") } val uri = LocalUriHandler.current - Box(modifier = Modifier.fillMaxSize()) { + 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 = "App Logo", + modifier = Modifier.size(200.dp), + contentScale = ContentScale.Inside + ) + + Spacer(modifier = Modifier.height(40.dp)) + + var showPassword by remember { + mutableStateOf(false) + } + + OutlinedTextField( + value = key.value, + onValueChange = { key.value = it }, + keyboardOptions = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Ascii, + imeAction = ImeAction.Go + ), + placeholder = { + Text( + text = "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) "Show Password" else "Hide Password" + ) + } + }, + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + keyboardActions = KeyboardActions( + onGo = { + try { + accountViewModel.login(key.value.text) + } catch (e: Exception) { + errorMessage = "Invalid key" + } + } + ) + ) + if (errorMessage.isNotBlank()) { + Text( + text = errorMessage, + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.caption + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = acceptedTerms.value, + onCheckedChange = { acceptedTerms.value = it } + ) + + Text(text = "I accept the ") + + ClickableText( + text = AnnotatedString("terms of use"), + onClick = { runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") } }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary), + ) + } + + 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 = "Acceptance of terms is required" + } + + if (key.value.text.isBlank()) { + errorMessage = "Key is required" + } + + if (acceptedTerms.value && key.value.text.isNotBlank()) { + try { + accountViewModel.login(key.value.text) + } catch (e: Exception) { + errorMessage = "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 = "Login") + } + } + } + + // The last child is glued to the bottom. ClickableText( text = AnnotatedString("Generate a new key"), modifier = Modifier - .align(Alignment.BottomCenter) - .padding(20.dp), + .padding(20.dp) + .fillMaxWidth(), onClick = { if (acceptedTerms.value) { accountViewModel.newKey() @@ -75,127 +185,9 @@ fun LoginPage(accountViewModel: AccountStateViewModel) { style = TextStyle( fontSize = 14.sp, textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.primary + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center ) ) } - Column( - modifier = Modifier.padding(20.dp).fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - - Image( - painterResource(id = R.drawable.amethyst), - contentDescription = "App Logo", - modifier = Modifier.size(200.dp), - contentScale = ContentScale.Inside - ) - - Spacer(modifier = Modifier.height(40.dp)) - - var showPassword by remember { - mutableStateOf(false) - } - - OutlinedTextField( - value = key.value, - onValueChange = { key.value = it }, - keyboardOptions = KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.Ascii, - imeAction = ImeAction.Go - ), - placeholder = { - Text( - text = "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) "Show Password" else "Hide Password" - ) - } - }, - visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - keyboardActions = KeyboardActions( - onGo = { - try { - accountViewModel.login(key.value.text) - } catch (e: Exception) { - errorMessage = "Invalid key" - } - } - ) - ) - if (errorMessage.isNotBlank()) { - Text( - text = errorMessage, - color = MaterialTheme.colors.error, - style = MaterialTheme.typography.caption - ) - } - - Spacer(modifier = Modifier.height(20.dp)) - - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox( - checked = acceptedTerms.value, - onCheckedChange = { acceptedTerms.value = it } - ) - - Text(text = "I accept the ") - - ClickableText( - text = AnnotatedString("terms of use"), - onClick = { runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") } }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary), - ) - } - - 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 = "Acceptance of terms is required" - } - - if (key.value.text.isBlank()) { - errorMessage = "Key is required" - } - - if (acceptedTerms.value && key.value.text.isNotBlank()) { - try { - accountViewModel.login(key.value.text) - } catch (e: Exception) { - errorMessage = "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 = "Login") - } - } - } } \ No newline at end of file