mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 14:15:55 +00:00
ui: smaller QR code for short links (#5946)
* ui: smaller QR code for short links * more small * size * translations
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user