From bff6723ba6233a37916f9d5224eda50a0590e290 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 5 Mar 2026 14:25:10 +0200 Subject: [PATCH] revert: remove QR code scanning from login Co-Authored-By: Claude Opus 4.6 --- desktopApp/build.gradle.kts | 4 - .../amethyst/desktop/ui/auth/LoginCard.kt | 49 +---- .../amethyst/desktop/ui/auth/QrCodeDecoder.kt | 181 ------------------ gradle/libs.versions.toml | 1 - 4 files changed, 8 insertions(+), 227 deletions(-) delete mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/QrCodeDecoder.kt diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 50fdb015b..792e02152 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -46,10 +46,6 @@ dependencies { // JSON implementation(libs.jackson.module.kotlin) - // QR code scanning (webcam via ffmpeg + ZXing decoding) - implementation(libs.zxing) - implementation(libs.zxing.javase) - // Collections implementation(libs.kotlinx.collections.immutable) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt index c64b71f58..39d56a42c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt @@ -30,14 +30,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.QrCodeScanner import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -117,43 +113,14 @@ fun LoginCard( Spacer(Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - KeyInputField( - value = keyInput, - onValueChange = { - keyInput = it - errorMessage = null - }, - errorMessage = errorMessage, - modifier = Modifier.weight(1f), - ) - - IconButton( - onClick = { - scope.launch(Dispatchers.IO) { - val decoded = scanQrFromWebcam() - withContext(Dispatchers.Main) { - if (decoded != null) { - keyInput = decoded - errorMessage = null - } else { - errorMessage = "No QR code found" - } - } - } - }, - ) { - Icon( - Icons.Default.QrCodeScanner, - contentDescription = "Scan QR code", - tint = MaterialTheme.colorScheme.primary, - ) - } - } + KeyInputField( + value = keyInput, + onValueChange = { + keyInput = it + errorMessage = null + }, + errorMessage = errorMessage, + ) Spacer(Modifier.height(8.dp)) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/QrCodeDecoder.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/QrCodeDecoder.kt deleted file mode 100644 index ead84ef91..000000000 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/QrCodeDecoder.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.desktop.ui.auth - -import com.google.zxing.BinaryBitmap -import com.google.zxing.MultiFormatReader -import com.google.zxing.NotFoundException -import com.google.zxing.client.j2se.BufferedImageLuminanceSource -import com.google.zxing.common.HybridBinarizer -import java.awt.BorderLayout -import java.awt.Dimension -import java.awt.Graphics -import java.awt.image.BufferedImage -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import javax.imageio.ImageIO -import javax.swing.JDialog -import javax.swing.JFrame -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.SwingConstants -import javax.swing.SwingUtilities - -private fun decodeQrFromImage(image: BufferedImage): String? = - try { - val source = BufferedImageLuminanceSource(image) - val bitmap = BinaryBitmap(HybridBinarizer(source)) - MultiFormatReader().decode(bitmap).text - } catch (_: NotFoundException) { - null - } - -private fun findFfmpeg(): String? = - listOf("ffmpeg", "/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg") - .firstOrNull { path -> - try { - ProcessBuilder(path, "-version") - .redirectErrorStream(true) - .start() - .waitFor() == 0 - } catch (_: Exception) { - false - } - } - -/** - * Opens a webcam scanner dialog using ffmpeg to capture frames. - * Continuously decodes frames looking for QR codes. - * Returns decoded text when found, or null if cancelled / no webcam. - * Must be called off the EDT (e.g. Dispatchers.IO). - */ -fun scanQrFromWebcam(): String? { - val ffmpeg = findFfmpeg() ?: return null - - val result = AtomicReference(null) - val running = AtomicBoolean(true) - - val imagePanel = - object : JPanel() { - var currentImage: BufferedImage? = null - - override fun paintComponent(g: Graphics) { - super.paintComponent(g) - currentImage?.let { img -> - val scale = - minOf( - width.toDouble() / img.width, - height.toDouble() / img.height, - ) - val w = (img.width * scale).toInt() - val h = (img.height * scale).toInt() - val x = (width - w) / 2 - val y = (height - h) / 2 - g.drawImage(img, x, y, w, h, null) - } - } - } - - val statusLabel = JLabel("Point camera at QR code...", SwingConstants.CENTER) - - val dialog = JDialog(null as JFrame?, "Scan QR Code", true) - dialog.defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE - dialog.layout = BorderLayout() - dialog.add(imagePanel, BorderLayout.CENTER) - dialog.add(statusLabel, BorderLayout.SOUTH) - dialog.preferredSize = Dimension(640, 520) - dialog.pack() - dialog.setLocationRelativeTo(null) - - dialog.addWindowListener( - object : java.awt.event.WindowAdapter() { - override fun windowClosing(e: java.awt.event.WindowEvent?) { - running.set(false) - } - }, - ) - - // Capture thread — runs ffmpeg to grab single frames in a loop - val captureThread = - Thread { - while (running.get()) { - try { - val process = - ProcessBuilder( - ffmpeg, - "-f", - "avfoundation", - "-video_size", - "640x480", - "-framerate", - "15", - "-i", - "0", - "-frames:v", - "1", - "-f", - "image2pipe", - "-vcodec", - "mjpeg", - "-q:v", - "5", - "pipe:1", - ).redirectError(ProcessBuilder.Redirect.DISCARD) - .start() - - val baos = ByteArrayOutputStream() - process.inputStream.use { it.copyTo(baos) } - process.waitFor() - - val bytes = baos.toByteArray() - if (bytes.isEmpty()) continue - - val image = ImageIO.read(ByteArrayInputStream(bytes)) ?: continue - - // Update preview - imagePanel.currentImage = image - SwingUtilities.invokeLater { imagePanel.repaint() } - - // Try to decode QR - val decoded = decodeQrFromImage(image) - if (decoded != null) { - result.set(decoded) - running.set(false) - SwingUtilities.invokeLater { dialog.dispose() } - break - } - } catch (_: Exception) { - if (!running.get()) break - Thread.sleep(500) - } - } - } - captureThread.isDaemon = true - captureThread.start() - - // Blocks until dialog is closed (modal) - dialog.isVisible = true - running.set(false) - - return result.get() -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e75f8df0..95a41277f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -160,7 +160,6 @@ vico-charts-views = { group = "com.patrykandpatrick.vico", name = "views", versi zelory-image-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" } zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" } zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } -zxing-javase = { group = "com.google.zxing", name = "javase", version.ref = "zxing" } zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" } androidx-window-core-android = { group = "androidx.window", name = "window-core-android", version.ref = "windowCoreAndroid" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }