- Improves rendering of the QR Code elements

- Makes the QR bigger
- Moves QR Code to rounder corners
- Moves QR Screen to use the arrow icon instead of the close button
This commit is contained in:
Vitor Pamplona
2024-08-27 11:04:13 -04:00
parent ceaa20fabf
commit 2b5e62aaf7
4 changed files with 51 additions and 25 deletions

View File

@@ -108,11 +108,11 @@ fun FollowingIcon(modifier: Modifier) {
} }
@Composable @Composable
fun ArrowBackIcon() { fun ArrowBackIcon(tint: Color = MaterialTheme.colorScheme.grayText) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringRes(R.string.back), contentDescription = stringRes(R.string.back),
tint = MaterialTheme.colorScheme.grayText, tint = tint,
) )
} }

View File

@@ -26,10 +26,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
@@ -46,13 +47,16 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.google.zxing.qrcode.encoder.ByteMatrix import com.google.zxing.qrcode.encoder.ByteMatrix
import com.google.zxing.qrcode.encoder.Encoder import com.google.zxing.qrcode.encoder.Encoder
import com.google.zxing.qrcode.encoder.QRCode import com.google.zxing.qrcode.encoder.QRCode
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
const val QR_MARGIN_PX = 100f const val QR_MARGIN_PX = 100f
@Preview @Preview
@Composable @Composable
fun QrCodeDrawerPreview() { fun QrCodeDrawerPreview() {
Box(Modifier.background(Color.Black).padding(10.dp)) {
QrCodeDrawer("Test QR data") QrCodeDrawer("Test QR data")
}
} }
@Composable @Composable
@@ -62,19 +66,21 @@ fun QrCodeDrawer(
) { ) {
val qrCode = remember(contents) { createQrCode(contents = contents) } val qrCode = remember(contents) { createQrCode(contents = contents) }
val foregroundColor = MaterialTheme.colorScheme.onSurface val foregroundColor = Color.Black
Box( Box(
modifier = modifier =
modifier modifier
.clip(shape = QuoteBorder)
.defaultMinSize(48.dp, 48.dp) .defaultMinSize(48.dp, 48.dp)
.aspectRatio(1f) .aspectRatio(1f)
.background(MaterialTheme.colorScheme.background), .background(Color.White),
) { ) {
Canvas(modifier = Modifier.fillMaxSize()) { Canvas(modifier = Modifier.fillMaxSize()) {
// Calculate the height and width of each column/row // Calculate the height and width of each column/row
val rowHeight = (size.width - QR_MARGIN_PX * 2f) / qrCode.matrix.height val rowHeight = (size.width - QR_MARGIN_PX * 2f) / qrCode.matrix.height
val columnWidth = (size.width - QR_MARGIN_PX * 2f) / qrCode.matrix.width val columnWidth = (size.width - QR_MARGIN_PX * 2f) / qrCode.matrix.width
val radius = CornerRadius(20f)
// Draw all of the finder patterns required by the QR spec. Calculate the ratio // Draw all of the finder patterns required by the QR spec. Calculate the ratio
// of the number of rows/columns to the width and height // of the number of rows/columns to the width and height
@@ -86,6 +92,7 @@ fun QrCodeDrawer(
height = rowHeight * FINDER_PATTERN_ROW_COUNT, height = rowHeight * FINDER_PATTERN_ROW_COUNT,
), ),
color = foregroundColor, color = foregroundColor,
cornerRadius = radius,
) )
// Draw data bits (encoded data part) // Draw data bits (encoded data part)
@@ -162,6 +169,7 @@ fun DrawScope.drawAllQrCodeDataBits(
), ),
), ),
).forEach { section -> ).forEach { section ->
val newSize = Size(size.width + 0.5f, size.height + 0.5f)
for (y in section.first.second until section.second.second) { for (y in section.first.second until section.second.second) {
for (x in section.first.first until section.second.first) { for (x in section.first.first until section.second.first) {
if (bytes[x, y] == 1.toByte()) { if (bytes[x, y] == 1.toByte()) {
@@ -177,7 +185,7 @@ fun DrawScope.drawAllQrCodeDataBits(
x = QR_MARGIN_PX + x * size.width, x = QR_MARGIN_PX + x * size.width,
y = QR_MARGIN_PX + y * size.height, y = QR_MARGIN_PX + y * size.height,
), ),
size = size, size = newSize,
), ),
) )
}, },
@@ -191,7 +199,7 @@ fun DrawScope.drawAllQrCodeDataBits(
const val FINDER_PATTERN_ROW_COUNT = 7 const val FINDER_PATTERN_ROW_COUNT = 7
private const val INTERIOR_EXTERIOR_SHAPE_RATIO = 3f / FINDER_PATTERN_ROW_COUNT private const val INTERIOR_EXTERIOR_SHAPE_RATIO = 3f / FINDER_PATTERN_ROW_COUNT
private const val INTERIOR_EXTERIOR_OFFSET_RATIO = 2f / FINDER_PATTERN_ROW_COUNT private const val INTERIOR_EXTERIOR_OFFSET_RATIO = 2f / FINDER_PATTERN_ROW_COUNT
private const val INTERIOR_EXTERIOR_SHAPE_CORNER_RADIUS = 0.12f private const val INTERIOR_EXTERIOR_SHAPE_CORNER_RADIUS = 0.50f
private const val INTERIOR_BACKGROUND_EXTERIOR_SHAPE_RATIO = 5f / FINDER_PATTERN_ROW_COUNT private const val INTERIOR_BACKGROUND_EXTERIOR_SHAPE_RATIO = 5f / FINDER_PATTERN_ROW_COUNT
private const val INTERIOR_BACKGROUND_EXTERIOR_OFFSET_RATIO = 1f / FINDER_PATTERN_ROW_COUNT private const val INTERIOR_BACKGROUND_EXTERIOR_OFFSET_RATIO = 1f / FINDER_PATTERN_ROW_COUNT
private const val INTERIOR_BACKGROUND_EXTERIOR_SHAPE_CORNER_RADIUS = 0.5f private const val INTERIOR_BACKGROUND_EXTERIOR_SHAPE_CORNER_RADIUS = 0.5f
@@ -206,6 +214,7 @@ private const val INTERIOR_BACKGROUND_EXTERIOR_SHAPE_CORNER_RADIUS = 0.5f
internal fun DrawScope.drawQrCodeFinders( internal fun DrawScope.drawQrCodeFinders(
sideLength: Float, sideLength: Float,
finderPatternSize: Size, finderPatternSize: Size,
cornerRadius: CornerRadius,
color: Color, color: Color,
) { ) {
setOf( setOf(
@@ -219,7 +228,7 @@ internal fun DrawScope.drawQrCodeFinders(
drawQrCodeFinder( drawQrCodeFinder(
topLeft = offset, topLeft = offset,
finderPatternSize = finderPatternSize, finderPatternSize = finderPatternSize,
cornerRadius = CornerRadius.Zero, cornerRadius = cornerRadius,
color = color, color = color,
) )
} }

View File

@@ -31,8 +31,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -54,14 +54,16 @@ import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05 import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
@@ -87,6 +89,15 @@ fun ShowQRDialogPreview() {
) )
} }
@Composable
fun BackButton(onPress: () -> Unit) {
IconButton(
onClick = onPress,
) {
ArrowBackIcon(MaterialTheme.colorScheme.onBackground)
}
}
@Composable @Composable
fun ShowQRDialog( fun ShowQRDialog(
user: User, user: User,
@@ -107,7 +118,7 @@ fun ShowQRDialog(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
CloseButton(onPress = onClose) BackButton(onPress = onClose)
} }
Column( Column(
@@ -126,23 +137,23 @@ fun ShowQRDialog(
contentDescription = stringRes(R.string.profile_image), contentDescription = stringRes(R.string.profile_image),
modifier = modifier =
Modifier Modifier
.width(100.dp) .width(120.dp)
.height(100.dp) .height(120.dp)
.clip(shape = CircleShape) .clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.background, CircleShape), .border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape),
loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
) )
} }
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 5.dp), modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) { ) {
CreateTextWithEmoji( CreateTextWithEmoji(
text = user.info?.bestName() ?: "", text = user.info?.bestName() ?: "",
tags = user.info?.tags, tags = user.info?.tags,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 18.sp, fontSize = 20.sp,
) )
} }
@@ -160,7 +171,7 @@ fun ShowQRDialog(
} else { } else {
Text( Text(
text = user.pubkeyDisplayHex(), text = user.pubkeyDisplayHex(),
fontSize = 14.sp, fontSize = Font14SP,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
@@ -170,20 +181,16 @@ fun ShowQRDialog(
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = Size35dp), modifier = Modifier.fillMaxWidth().padding(horizontal = Size10dp),
) { ) {
QrCodeDrawer(user.toNostrUri()) QrCodeDrawer(user.toNostrUri())
} }
Row(modifier = Modifier.padding(horizontal = 30.dp)) { Row(modifier = Modifier.padding(horizontal = 30.dp)) {
Button( FilledTonalButton(
onClick = { presenting = false }, onClick = { presenting = false },
shape = RoundedCornerShape(Size35dp), shape = RoundedCornerShape(Size35dp),
modifier = Modifier.fillMaxWidth().height(50.dp), modifier = Modifier.fillMaxWidth().height(50.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) { ) {
Text(text = stringRes(R.string.scan_qr)) Text(text = stringRes(R.string.scan_qr))
} }

View File

@@ -91,6 +91,16 @@ class Nip19Bech32Test {
Assert.assertEquals(null, actual) Assert.assertEquals(null, actual)
} }
@Test()
fun uri_to_route_complete_nprofile_2() {
val actual = Nip19Bech32.uriToRoute("nostr:nprofile1qqsyvrp9u6p0mfur9dfdru3d853tx9mdjuhkphxuxgfwmryja7zsvhqpzamhxue69uhhv6t5daezumn0wd68yvfwvdhk6tcpz9mhxue69uhkummnw3ezuamfdejj7qgwwaehxw309ahx7uewd3hkctcscpyug")
Assert.assertNotNull(actual)
Assert.assertTrue(actual?.entity is Nip19Bech32.NProfile)
Assert.assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", (actual?.entity as? Nip19Bech32.NProfile)?.hex)
Assert.assertEquals("wss://vitor.nostr1.com/", (actual?.entity as? Nip19Bech32.NProfile)?.relay?.first())
}
@Test() @Test()
fun uri_to_route_complete_nprofile() { fun uri_to_route_complete_nprofile() {
val actual = Nip19Bech32.uriToRoute("nostr:nprofile1qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7qgwwaehxw309ahx7uewd3hkctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uqzq9thu3vem5gvsc6f3l3uyz7c92h6lq56t9wws0zulzkrgc6nrvym5jfztf") val actual = Nip19Bech32.uriToRoute("nostr:nprofile1qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7qgwwaehxw309ahx7uewd3hkctcpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uqzq9thu3vem5gvsc6f3l3uyz7c92h6lq56t9wws0zulzkrgc6nrvym5jfztf")