ui: smaller QR code for short links (#5946)

* ui: smaller QR code for short links

* more small

* size

* translations
This commit is contained in:
Evgeny
2025-05-25 11:56:00 +01:00
committed by GitHub
parent ee2ea152dc
commit b848f735ce
9 changed files with 46 additions and 31 deletions

View File

@@ -12,11 +12,12 @@ import SimpleXChat
struct MutableQRCode: View {
@Binding var uri: String
var small: Bool = false
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var body: some View {
QRCode(uri: uri, withLogo: withLogo, tintColor: tintColor)
QRCode(uri: uri, small: small, withLogo: withLogo, tintColor: tintColor)
.id("simplex-qrcode-view-for-\(uri)")
}
}
@@ -27,7 +28,7 @@ struct SimpleXCreatedLinkQRCode: View {
var onShare: (() -> Void)? = nil
var body: some View {
QRCode(uri: link.simplexChatUri(short: short), onShare: onShare)
QRCode(uri: link.simplexChatUri(short: short), small: short && link.connShortLink != nil, onShare: onShare)
}
}
@@ -38,50 +39,57 @@ struct SimpleXLinkQRCode: View {
var onShare: (() -> Void)? = nil
var body: some View {
QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor, onShare: onShare)
QRCode(uri: simplexChatLink(uri), small: uri.count < 200, withLogo: withLogo, tintColor: tintColor, onShare: onShare)
}
}
private let smallQRRatio: CGFloat = 0.63
struct QRCode: View {
let uri: String
var small: Bool = false
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
var onShare: (() -> Void)? = nil
@State private var image: UIImage? = nil
@State private var makeScreenshotFunc: () -> Void = {}
@State private var width: CGFloat = .infinity
var body: some View {
ZStack {
if let image = image {
qrCodeImage(image)
GeometryReader { geo in
qrCodeImage(image).frame(width: width, height: width)
GeometryReader { g in
let w = g.size.width * (small ? smallQRRatio : 1)
let l = w * (small ? 0.195 : 0.16)
let m = w * 0.005
ZStack {
if withLogo {
let w = geo.size.width
Image("icon-light")
.resizable()
.scaledToFit()
.frame(width: w * 0.16, height: w * 0.16)
.frame(width: w * 0.165, height: w * 0.165)
.frame(width: l, height: l)
.frame(width: l + m, height: l + m)
.background(.white)
.clipShape(Circle())
}
}
.onAppear {
width = w
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
showShareSheet(items: [makeScreenshot(g.frame(in: .local).origin, size)])
onShare?()
}
}
.frame(width: geo.size.width, height: geo.size.height)
.frame(width: g.size.width, height: g.size.height)
}
} else {
Color.clear.aspectRatio(1, contentMode: .fit)
Color.clear.aspectRatio(small ? 1 / smallQRRatio : 1, contentMode: .fit)
}
}
.onTapGesture(perform: makeScreenshotFunc)
.task { image = await generateImage(uri, tintColor: tintColor) }
.task { image = await generateImage(uri, tintColor: tintColor, errorLevel: small ? "M" : "L") }
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -94,10 +102,11 @@ private func qrCodeImage(_ image: UIImage) -> some View {
.textSelection(.enabled)
}
private func generateImage(_ uri: String, tintColor: UIColor) async -> UIImage? {
private func generateImage(_ uri: String, tintColor: UIColor, errorLevel: String) async -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8)
filter.correctionLevel = errorLevel
if let outputImage = filter.outputImage,
let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor)

View File

@@ -65,7 +65,7 @@ struct NewServerView: View {
useServerSection(valid)
if valid {
Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) {
MutableQRCode(uri: $serverToEdit.server)
MutableQRCode(uri: $serverToEdit.server, small: true)
.listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
}
}

View File

@@ -110,7 +110,7 @@ struct ProtocolServerView: View {
useServerSection(valid)
if valid {
Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) {
MutableQRCode(uri: $serverToEdit.server)
MutableQRCode(uri: $serverToEdit.server, small: true)
.listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
}
}

View File

@@ -3797,7 +3797,7 @@ alert button */
"Or securely share this file link" = "Или передайте эту ссылку";
/* No comment provided by engineer. */
"Or show this code" = "Или покажите этот код";
"Or show this code" = "Или покажите код";
/* No comment provided by engineer. */
"Or to share privately" = "Или поделиться конфиденциально";
@@ -4844,7 +4844,7 @@ chat item action */
"Share SimpleX address on social media." = "Поделитесь SimpleX адресом в социальных сетях.";
/* No comment provided by engineer. */
"Share this 1-time invite link" = "Поделиться одноразовой ссылкой-приглашением";
"Share this 1-time invite link" = "Поделитесь одноразовой ссылкой";
/* No comment provided by engineer. */
"Share to SimpleX" = "Поделиться в SimpleX";

View File

@@ -101,13 +101,13 @@ actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this
actual fun ImageBitmap.hasAlpha(): Boolean = hasAlpha
actual fun ImageBitmap.addLogo(): ImageBitmap = asAndroidBitmap().applyCanvas {
val radius = (width * 0.16f) / 2
actual fun ImageBitmap.addLogo(size: Float): ImageBitmap = asAndroidBitmap().applyCanvas {
val radius = (width * size) / 2
val paint = android.graphics.Paint()
paint.color = android.graphics.Color.WHITE
drawCircle(width / 2f, height / 2f, radius, paint)
val logo = androidAppContext.resources.getDrawable(R.drawable.icon_foreground_android_common, null).toBitmap()
val logoSize = (width * 0.24).toInt()
val logoSize = (width * size * 1.5).toInt()
translate((width - logoSize) / 2f, (height - logoSize) / 2f)
drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null)
}.asImageBitmap()

View File

@@ -16,7 +16,7 @@ expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut
expect fun GrayU8.toImageBitmap(): ImageBitmap
expect fun ImageBitmap.hasAlpha(): Boolean
expect fun ImageBitmap.addLogo(): ImageBitmap
expect fun ImageBitmap.addLogo(size: Float): ImageBitmap
expect fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap
expect fun isImage(uri: URI): Boolean

View File

@@ -31,6 +31,7 @@ fun SimpleXCreatedLinkQRCode(
) {
QRCode(
connLink.simplexChatUri(short),
small = short && connLink.connShortLink != null,
modifier,
padding,
tintColor,
@@ -50,6 +51,7 @@ fun SimpleXLinkQRCode(
) {
QRCode(
simplexChatLink(connReq),
small = connReq.count() < 200,
modifier,
padding,
tintColor,
@@ -61,6 +63,7 @@ fun SimpleXLinkQRCode(
@Composable
fun QRCode(
connReq: String,
small: Boolean = false,
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF),
tintColor: Color = Color(0xff062d56),
@@ -68,9 +71,11 @@ fun QRCode(
onShare: (() -> Unit)? = null,
) {
val scope = rememberCoroutineScope()
val logoSize = if (small) 0.21f else 0.16f
val errorLevel = if (small) QrCode.ErrorLevel.M else QrCode.ErrorLevel.L
val qr = remember(connReq, tintColor, withLogo) {
qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo() else it }
qrCodeBitmap(connReq, 1024, errorLevel).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo(logoSize) else it }
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Image(
@@ -79,12 +84,13 @@ fun QRCode(
Modifier
.padding(padding)
.widthIn(max = 400.dp)
.fillMaxWidth(if (small) 0.67f else 1f)
.aspectRatio(1f)
.then(modifier)
.clickable {
scope.launch {
val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo() else it }
val image = qrCodeBitmap(connReq, 1024, errorLevel).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo(logoSize) else it }
val file = saveTempImageUncompressed(image, true)
if (file != null) {
shareFile("", CryptoFile.plain(file.absolutePath))
@@ -96,8 +102,8 @@ fun QRCode(
}
}
fun qrCodeBitmap(content: String, size: Int = 1024): ImageBitmap {
val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate()
fun qrCodeBitmap(content: String, size: Int = 1024, errorLevel: QrCode.ErrorLevel): ImageBitmap {
val qrCode = QrCodeEncoder().addAutomatic(content).setError(errorLevel).fixate()
/** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */
val numModules = QrCode.totalModules(qrCode.version)
// Hide border on light themes to make it fit to the same place as camera in QRCodeScanner.

View File

@@ -189,7 +189,7 @@ fun CustomServer(
if (valid.value) {
SectionDividerSpaced()
SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) {
QRCode(serverAddress.value)
QRCode(serverAddress.value, small = true)
}
}
}

View File

@@ -133,9 +133,9 @@ actual fun ImageBitmap.hasAlpha(): Boolean {
return false
}
actual fun ImageBitmap.addLogo(): ImageBitmap {
val radius = (width * 0.16f).toInt()
val logoSize = (width * 0.24).toInt()
actual fun ImageBitmap.addLogo(size: Float): ImageBitmap {
val radius = (width * size).toInt()
val logoSize = (width * size * 1.5).toInt()
val logo: BufferedImage = MR.images.icon_foreground_common.image
val original = toAwtImage()
val withLogo = BufferedImage(width, height, original.type)