diff --git a/README.md b/README.md index 59d029f..e3116ee 100644 --- a/README.md +++ b/README.md @@ -12,19 +12,19 @@ Supported Compose version: | Compose version | EasyQRScan Version | |-----------------|--------------------| -| 1.6.x | 0.1.x | -| 1.7 | Not yet supported | +| 1.6.x | 0.1.0+ | +| 1.7 | 0.2.0+ | # Dependency Add the dependency to your commonMain sourceSet (KMP) / Android dependencies (android only): ```kotlin -implementation("io.github.kalinjul.easyqrscan:scanner:0.1.6") +implementation("io.github.kalinjul.easyqrscan:scanner:0.2.0") ``` Or, for your libs.versions.toml: ```toml [versions] -easyqrscan = "0.1.6" +easyqrscan = "0.2.0" [libraries] easyqrscan = { module = "io.github.kalinjul.easyqrscan:scanner", version.ref = "easyqrscan" } ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d84602c..1c321fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,16 +8,16 @@ jvmTarget = "17" agp = "8.4.2" #https://github.com/JetBrains/compose-multiplatform -compose-multiplatform = "1.6.11" -kotlin = "2.0.0" +compose-multiplatform = "1.7.0" +kotlin = "2.0.20" # https://github.com/google/ksp -ksp = "2.0.0-1.0.21" +ksp = "2.0.20-1.0.25" #https://mvnrepository.com/artifact/org.jetbrains.compose.compiler/compiler #composeCompiler = "1.5.8.1" # https://developer.android.com/jetpack/androidx/releases/activity -androidxActivity = "1.9.0" +androidxActivity = "1.9.3" # https://developer.android.com/jetpack/androidx/releases/appcompat androidxAppCompat = "1.7.0" coreKtx = "1.13.1" @@ -25,8 +25,8 @@ coreKtx = "1.13.1" dokka = "1.9.10" nexus-publish-plugin = "1.3.0" accompanist = "0.34.0" -androidxCamera = "1.3.3" -mlkit = "17.2.0" +androidxCamera = "1.3.4" +mlkit = "17.3.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } diff --git a/sample-app/shared/src/commonMain/kotlin/MainView.kt b/sample-app/shared/src/commonMain/kotlin/MainView.kt index da777a1..3724c3e 100644 --- a/sample-app/shared/src/commonMain/kotlin/MainView.kt +++ b/sample-app/shared/src/commonMain/kotlin/MainView.kt @@ -1,34 +1,55 @@ package org.publicvalue.multiplatform.qrcode.sample +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.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.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import org.publicvalue.multiplatform.qrcode.CodeType import org.publicvalue.multiplatform.qrcode.ScannerWithPermissions @Composable fun MainView() { - Column() { - Text("Scan QR-Code below") - var scannerVisible by remember {mutableStateOf(false)} - Button(onClick = { - scannerVisible = !scannerVisible - }) { - Text("Toggle scanner (visible: $scannerVisible)") - } - if (scannerVisible) { - ScannerWithPermissions( - modifier = Modifier.padding(16.dp), - onScanned = { println(it); true }, types = listOf(CodeType.QR) - ) + Box() { + val snackbarHostState = remember() { SnackbarHostState() } + + Column() { + Text("Scan QR-Code below") + var scannerVisible by remember {mutableStateOf(false)} + Button(onClick = { + scannerVisible = !scannerVisible + }) { + Text("Toggle scanner (visible: $scannerVisible)") + } + if (scannerVisible) { + val scope = rememberCoroutineScope() + ScannerWithPermissions( + modifier = Modifier.padding(16.dp), + onScanned = { + scope.launch { + snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Short) + } + false // continue scanning + }, types = listOf(CodeType.QR) + ) + } } + SnackbarHost( + modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 20.dp), + hostState = snackbarHostState + ) } } \ No newline at end of file diff --git a/scanner/src/commonMain/kotlin/Scanner.kt b/scanner/src/commonMain/kotlin/Scanner.kt index c8e5d46..3f2db2d 100644 --- a/scanner/src/commonMain/kotlin/Scanner.kt +++ b/scanner/src/commonMain/kotlin/Scanner.kt @@ -11,6 +11,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.unit.dp +/** + * Code Scanner + * + * @param types Code types to scan. + * @param onScanned Called when a code was scanned. The given lambda should return true + * if scanning was successful and scanning should be aborted. + * Return false if scanning should continue. + */ @Composable expect fun Scanner( modifier: Modifier = Modifier, @@ -18,16 +26,26 @@ expect fun Scanner( types: List ) +/** + * Code Scanner with permission handling. + * + * @param types Code types to scan. + * @param onScanned Called when a code was scanned. The given lambda should return true + * if scanning was successful and scanning should be aborted. + * Return false if scanning should continue. + * @param permissionText Text to show if permission was denied. + * @param openSettingsLabel Label to show on the "Go to settings" Button + */ @Composable fun ScannerWithPermissions( - modifier: Modifier = Modifier.clipToBounds(), + modifier: Modifier = Modifier, onScanned: (String) -> Boolean, types: List, permissionText: String = "Camera is required for QR Code scanning", openSettingsLabel: String = "Open Settings", ) { ScannerWithPermissions( - modifier = modifier, + modifier = modifier.clipToBounds(), onScanned = onScanned, types = types, permissionDeniedContent = { permissionState -> @@ -44,6 +62,15 @@ fun ScannerWithPermissions( ) } +/** + * Code Scanner with permission handling. + * + * @param types Code types to scan. + * @param onScanned Called when a code was scanned. The given lambda should return true + * if scanning was successful and scanning should be aborted. + * Return false if scanning should continue. + * @param permissionDeniedContent Content to show if permission was denied. + */ @Composable fun ScannerWithPermissions( modifier: Modifier = Modifier, diff --git a/scanner/src/iosMain/kotlin/Scanner.ios.kt b/scanner/src/iosMain/kotlin/Scanner.ios.kt index f17d59e..a500c1a 100644 --- a/scanner/src/iosMain/kotlin/Scanner.ios.kt +++ b/scanner/src/iosMain/kotlin/Scanner.ios.kt @@ -15,7 +15,6 @@ import platform.Foundation.NSURL import platform.UIKit.UIApplication import platform.UIKit.UIApplicationOpenSettingsURLString - @Composable actual fun Scanner( modifier: Modifier, diff --git a/scanner/src/iosMain/kotlin/ScannerView.kt b/scanner/src/iosMain/kotlin/ScannerView.kt index 15a68d4..9e360c8 100644 --- a/scanner/src/iosMain/kotlin/ScannerView.kt +++ b/scanner/src/iosMain/kotlin/ScannerView.kt @@ -5,13 +5,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.interop.UIKitView +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.CValue import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ObjCObjectVar import kotlinx.cinterop.alloc +import kotlinx.cinterop.cValue import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr import kotlinx.cinterop.useContents @@ -33,9 +34,9 @@ import platform.AVFoundation.AVLayerVideoGravityResizeAspectFill import platform.AVFoundation.AVMediaTypeVideo import platform.AVFoundation.AVMetadataMachineReadableCodeObject import platform.AVFoundation.AVMetadataObjectType -import platform.AVFoundation.AVMetadataObjectTypeQRCode import platform.AudioToolbox.AudioServicesPlaySystemSound import platform.CoreGraphics.CGRect +import platform.CoreGraphics.CGRectZero import platform.Foundation.NSError import platform.QuartzCore.CALayer import platform.QuartzCore.CATransaction @@ -46,7 +47,6 @@ import platform.UIKit.UIView import platform.darwin.NSObject import platform.darwin.dispatch_get_main_queue -@OptIn(ExperimentalForeignApi::class) @Composable fun UiScannerView( modifier: Modifier = Modifier, @@ -72,24 +72,18 @@ fun UiScannerView( } } - UIKitView( + UIKitView( modifier = modifier.fillMaxSize(), - background = Color.Black, factory = { - val previewContainer = UIView() + val previewContainer = ScannerPreviewView(coordinator) println("Calling prepare") coordinator.prepare(previewContainer.layer, allowedMetadataTypes) previewContainer }, - update = { - }, - onResize = { view, rect -> - CATransaction.begin() - CATransaction.setValue(true, kCATransactionDisableActions) - view.layer.setFrame(rect) - coordinator.setFrame(rect) - CATransaction.commit() - } + properties = UIKitInteropProperties( + isInteractive = true, + isNativeAccessibilityEnabled = true, + ) ) // DisposableEffect(Unit) { @@ -101,6 +95,20 @@ fun UiScannerView( } +@OptIn(ExperimentalForeignApi::class) +class ScannerPreviewView(private val coordinator: ScannerCameraCoordinator): UIView(frame = cValue { CGRectZero }) { + @OptIn(ExperimentalForeignApi::class) + override fun layoutSubviews() { + super.layoutSubviews() + CATransaction.begin() + CATransaction.setValue(true, kCATransactionDisableActions) + + layer.setFrame(frame) + coordinator.setFrame(frame) + CATransaction.commit() + } +} + @OptIn(ExperimentalForeignApi::class) class ScannerCameraCoordinator( val onScanned: (String) -> Boolean @@ -195,8 +203,6 @@ class ScannerCameraCoordinator( } fun onFound(code: String) { - // kSystemSoundID_UserPreferredAlert = 0x00001000 - AudioServicesPlaySystemSound(0x1000u) // Mail-Sound 1108 wäre der Photo Sound captureSession.stopRunning() if (!onScanned(code)) { GlobalScope.launch(Dispatchers.Default) {