Support for PubKey QR showing and Scanning.

pull/55/head
Vitor Pamplona 2023-01-29 00:56:13 -03:00
rodzic d1753a59f9
commit 9263684031
6 zmienionych plików z 681 dodań i 4 usunięć

Wyświetl plik

@ -110,6 +110,14 @@ dependencies {
implementation "com.google.accompanist:accompanist-pager:$accompanist_version" // Pager
implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version"
// For QR generation
implementation "com.google.zxing:core:3.5.0"
implementation "androidx.camera:camera-camera2:1.2.1"
implementation 'androidx.camera:camera-lifecycle:1.2.1'
implementation 'androidx.camera:camera-view:1.2.1'
implementation 'com.google.mlkit:vision-common:17.3.0'
implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

Wyświetl plik

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
@ -17,15 +18,18 @@
android:largeHeap="true"
tools:targetApi="31">
<activity
android:label="Amethyst"
android:name=".ui.MainActivity"
android:exported="true"
android:launchMode="singleTask" android:allowTaskReparenting="true"
android:theme="@style/Theme.Amethyst">
<intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@ -36,6 +40,10 @@
android:name="android.app.lib_name"
android:value="" />
</activity>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode" />
</application>
</manifest>

Wyświetl plik

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -7,6 +8,9 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -16,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ScaffoldState
import androidx.compose.material.Surface
@ -23,7 +28,10 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -34,15 +42,35 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontWeight.Companion.W500
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.google.zxing.qrcode.encoder.Encoder
import com.google.zxing.qrcode.encoder.QRCode
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.components.ZoomableAsyncImage
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
import nostr.postr.toNpub
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.DrawScope
import com.google.zxing.qrcode.encoder.ByteMatrix
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
@Composable
fun DrawerContent(navController: NavHostController,
@ -54,7 +82,7 @@ fun DrawerContent(navController: NavHostController,
val account = accountState?.account ?: return
val accountUserState by account.userProfile().live.observeAsState()
val accountUser = accountUserState?.user
val accountUser = accountUserState?.user ?: return
Surface(
modifier = Modifier.fillMaxWidth(),
@ -106,6 +134,8 @@ fun DrawerContent(navController: NavHostController,
.weight(1F),
accountStateViewModel
)
BottomContent(accountUser, scaffoldState, navController)
}
}
}
@ -239,5 +269,73 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState
)
}
}
}
@Composable
fun BottomContent(user: User, scaffoldState: ScaffoldState, navController: NavController) {
val coroutineScope = rememberCoroutineScope()
// store the dialog open or close state
var dialogOpen by remember {
mutableStateOf(false)
}
Column(modifier = Modifier) {
Divider(
modifier = Modifier.padding(top = 15.dp),
thickness = 0.25.dp
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp),
) {
/*
IconButton(
onClick = {
when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_NO -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
AppCompatDelegate.MODE_NIGHT_YES -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}
) {
Icon(
painter = painterResource(R.drawable.ic_theme),
null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary
)
}*/
Box(modifier = Modifier.weight(1F))
IconButton(onClick = {
dialogOpen = true
coroutineScope.launch {
scaffoldState.drawerState.close()
}
}) {
Icon(
painter = painterResource(R.drawable.ic_qrcode),
null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary
)
}
}
}
if (dialogOpen) {
ShowQRDialog(user,
onScan = {
dialogOpen = false
coroutineScope.launch {
scaffoldState.drawerState.close()
}
navController.navigate(it)
},
onClose = { dialogOpen = false }
)
}
}
}

Wyświetl plik

@ -0,0 +1,245 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.unit.dp
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.google.zxing.qrcode.encoder.ByteMatrix
import com.google.zxing.qrcode.encoder.Encoder
import com.google.zxing.qrcode.encoder.QRCode
const val QR_MARGIN_PX = 100f
@Composable
fun QrCodeDrawer(contents: String, modifier: Modifier = Modifier) {
val qrCode = remember(contents) {
createQrCode(contents = contents)
}
val foregroundColor = MaterialTheme.colors.onSurface
Box(
modifier = modifier
.defaultMinSize(48.dp, 48.dp)
.aspectRatio(1f)
.background(MaterialTheme.colors.background)
) {
Canvas(modifier = Modifier.fillMaxSize()) {
// Calculate the height and width of each column/row
val rowHeight = (size.width - QR_MARGIN_PX * 2f) / qrCode.matrix.height
val columnWidth = (size.width - QR_MARGIN_PX * 2f) / qrCode.matrix.width
// 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
drawQrCodeFinders(
sideLength = size.width,
finderPatternSize = Size(
width = columnWidth * FINDER_PATTERN_ROW_COUNT,
height = rowHeight * FINDER_PATTERN_ROW_COUNT
),
color = foregroundColor
)
// Draw data bits (encoded data part)
drawAllQrCodeDataBits(
bytes = qrCode.matrix,
size = Size(
width = columnWidth,
height = rowHeight
),
color = foregroundColor
)
}
}
}
private typealias Coordinate = Pair<Int, Int>
private fun createQrCode(contents: String): QRCode {
require(contents.isNotEmpty())
return Encoder.encode(
contents,
ErrorCorrectionLevel.Q,
mapOf(
EncodeHintType.CHARACTER_SET to "UTF-8",
EncodeHintType.MARGIN to QR_MARGIN_PX,
EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.Q
)
)
}
fun newPath(withPath: Path.() -> Unit) = Path().apply {
fillType = PathFillType.EvenOdd
withPath(this)
}
fun DrawScope.drawAllQrCodeDataBits(
bytes: ByteMatrix,
size: Size,
color: Color,
) {
setOf(
// data bits between top left finder pattern and top right finder pattern.
Pair(
first = Coordinate(first = FINDER_PATTERN_ROW_COUNT, second = 0),
second = Coordinate(
first = (bytes.width - FINDER_PATTERN_ROW_COUNT),
second = FINDER_PATTERN_ROW_COUNT
)
),
// data bits below top left finder pattern and above bottom left finder pattern.
Pair(
first = Coordinate(first = 0, second = FINDER_PATTERN_ROW_COUNT),
second = Coordinate(
first = bytes.width,
second = bytes.height - FINDER_PATTERN_ROW_COUNT
)
),
// data bits to the right of the bottom left finder pattern.
Pair(
first = Coordinate(
first = FINDER_PATTERN_ROW_COUNT,
second = (bytes.height - FINDER_PATTERN_ROW_COUNT)
),
second = Coordinate(
first = bytes.width,
second = bytes.height
)
)
).forEach { section ->
for (y in section.first.second until section.second.second) {
for (x in section.first.first until section.second.first) {
if (bytes[x, y] == 1.toByte()) {
drawPath(
color = color,
path = newPath {
addRect(
rect = Rect(
offset = Offset(
x = QR_MARGIN_PX + x * size.width,
y = QR_MARGIN_PX + y * size.height
),
size = size
)
)
}
)
}
}
}
}
}
const val FINDER_PATTERN_ROW_COUNT = 7
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_SHAPE_CORNER_RADIUS = 0.12f
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_SHAPE_CORNER_RADIUS = 0.5f
/**
* A valid QR code has three finder patterns (top left, top right, bottom left).
*
* @param qrCodeProperties how the QR code is drawn
* @param sideLength length, in pixels, of each side of the QR code
* @param finderPatternSize [Size] of each finder patten, based on the QR code spec
*/
internal fun DrawScope.drawQrCodeFinders(
sideLength: Float,
finderPatternSize: Size,
color: Color
) {
setOf(
// Draw top left finder pattern.
Offset(x = QR_MARGIN_PX, y = QR_MARGIN_PX),
// Draw top right finder pattern.
Offset(x = sideLength - (QR_MARGIN_PX + finderPatternSize.width), y = QR_MARGIN_PX),
// Draw bottom finder pattern.
Offset(x = QR_MARGIN_PX, y = sideLength - (QR_MARGIN_PX + finderPatternSize.height))
).forEach { offset ->
drawQrCodeFinder(
topLeft = offset,
finderPatternSize = finderPatternSize,
cornerRadius = CornerRadius.Zero,
color = color
)
}
}
/**
* This func is responsible for drawing a single finder pattern, for a QR code
*/
private fun DrawScope.drawQrCodeFinder(
topLeft: Offset,
finderPatternSize: Size,
cornerRadius: CornerRadius,
color: Color
) {
drawPath(
color = color,
path = newPath {
// Draw the outer rectangle for the finder pattern.
addRoundRect(
roundRect = RoundRect(
rect = Rect(
offset = topLeft,
size = finderPatternSize
),
cornerRadius = cornerRadius
)
)
// Draw background for the finder pattern interior (this keeps the arc ratio consistent).
val innerBackgroundOffset = Offset(
x = finderPatternSize.width * INTERIOR_BACKGROUND_EXTERIOR_OFFSET_RATIO,
y = finderPatternSize.height * INTERIOR_BACKGROUND_EXTERIOR_OFFSET_RATIO
)
addRoundRect(
roundRect = RoundRect(
rect = Rect(
offset = topLeft + innerBackgroundOffset,
size = finderPatternSize * INTERIOR_BACKGROUND_EXTERIOR_SHAPE_RATIO
),
cornerRadius = cornerRadius * INTERIOR_BACKGROUND_EXTERIOR_SHAPE_CORNER_RADIUS
)
)
// Draw the inner rectangle for the finder pattern.
val innerRectOffset = Offset(
x = finderPatternSize.width * INTERIOR_EXTERIOR_OFFSET_RATIO,
y = finderPatternSize.height * INTERIOR_EXTERIOR_OFFSET_RATIO
)
addRoundRect(
roundRect = RoundRect(
rect = Rect(
offset = topLeft + innerRectOffset,
size = finderPatternSize * INTERIOR_EXTERIOR_SHAPE_RATIO
),
cornerRadius = cornerRadius * INTERIOR_EXTERIOR_SHAPE_CORNER_RADIUS
)
)
}
)
}

Wyświetl plik

@ -0,0 +1,167 @@
package com.vitorpamplona.amethyst.ui.navigation
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.util.Size
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.common.util.concurrent.ListenableFuture
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import com.vitorpamplona.amethyst.service.Nip19
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@Composable
fun QrCodeScanner(onScan: (String) -> Unit) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val cameraExecutor= Executors.newSingleThreadExecutor()
var hasCameraPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
)
}
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { granted ->
hasCameraPermission = granted
}
)
val analyzer = QRCodeAnalyzer { result ->
result?.let {
try {
val nip19 = Nip19().uriToRoute(it)
val startingPage = when (nip19?.type) {
Nip19.Type.USER -> "User/${nip19.hex}"
Nip19.Type.NOTE -> "Note/${nip19.hex}"
else -> null
}
if (startingPage != null) {
onScan(startingPage)
}
} catch (e: Throwable) {
// QR can be anythign. do not throw errors.
}
}
}
DisposableEffect(key1 = true) {
launcher.launch(Manifest.permission.CAMERA)
onDispose() {
cameraProviderFuture.get().unbindAll()
cameraExecutor.shutdown()
}
}
Column() {
if (hasCameraPermission) {
AndroidView(
factory = { context ->
val previewView = PreviewView(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
bindPreview(analyzer, previewView, cameraExecutor, cameraProvider, lifecycleOwner)
}, ContextCompat.getMainExecutor(context))
return@AndroidView previewView
},
modifier = Modifier.weight(1f)
)
}
}
}
fun bindPreview(
analyzer: ImageAnalysis.Analyzer,
previewView: PreviewView,
cameraExecutor: ExecutorService,
cameraProvider: ProcessCameraProvider,
lifecycleOwner: LifecycleOwner
) {
val preview = Preview.Builder().build()
val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(
cameraExecutor,
analyzer
)
cameraProvider.bindToLifecycle(
lifecycleOwner,
selector,
imageAnalysis,
preview
)
}
class QRCodeAnalyzer(
private val onQrCodeScanned: (result: String?) -> Unit
) : ImageAnalysis.Analyzer {
private val scanningOptions = BarcodeScannerOptions.Builder().setBarcodeFormats(Barcode.FORMAT_QR_CODE).build()
fun scanBarcodes(inputImage: InputImage) {
BarcodeScanning.getClient(scanningOptions).process(inputImage)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
onQrCodeScanned(barcodes[0].displayValue)
}
}
.addOnFailureListener {
it.printStackTrace()
}
}
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
override fun analyze(imageProxy: ImageProxy) {
imageProxy.image?.let { image ->
val inputImage = InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees)
scanBarcodes(inputImage)
}
imageProxy.close()
}
}

Wyświetl plik

@ -0,0 +1,151 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import nostr.postr.toNpub
@Composable
fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
var presenting by remember { mutableStateOf(true) }
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize(),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = onClose)
}
Column(
modifier = Modifier.fillMaxSize().padding(10.dp),
verticalArrangement = Arrangement.Center
) {
if (presenting) {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
AsyncImage(
model = user.profilePicture() ?: "https://robohash.org/ohno.png",
contentDescription = "Profile Image",
placeholder = rememberAsyncImagePainter("https://robohash.org/${user.pubkeyHex}.png"),
modifier = Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colors.background, CircleShape)
.background(MaterialTheme.colors.background)
)
}
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
Text(
user.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp),
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
Text(" @${user.bestUsername()}", color = Color.LightGray)
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(30.dp)
) {
QrCodeDrawer("nostr:${user.pubkey.toNpub()}")
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(30.dp)
) {
Button(
onClick = { presenting = false },
shape = RoundedCornerShape(35.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = "Scan QR")
}
}
} else {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(30.dp)
) {
QrCodeScanner(onScan)
}
Button(
onClick = { presenting = true },
shape = RoundedCornerShape(35.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(text = "Show QR")
}
}
}
}
}
}
}