From b759f6fbe36b75dd7824fbb0567cb2c7634d025f Mon Sep 17 00:00:00 2001 From: "yevhen.kryvoshei" Date: Tue, 16 Sep 2025 11:55:19 +0300 Subject: [PATCH 1/7] Added skeleton code for screen and functions --- .../dev/sdkforge/camera/app/ScannerScreen.kt | 28 +++++++++++++++++++ shared-ui/build.gradle.kts | 1 + .../camera/ui/PlatformCameraView.android.kt | 16 +++++++++++ .../sdkforge/camera/ui/CameraController.kt | 24 ++++++++++++++++ .../sdkforge/camera/ui/PlatformCameraView.kt | 8 ++++++ .../camera/ui/PlatformCameraView.ios.kt | 16 +++++++++++ 6 files changed, 93 insertions(+) create mode 100644 app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt new file mode 100644 index 0000000..c5d6de3 --- /dev/null +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt @@ -0,0 +1,28 @@ +package dev.sdkforge.camera.app + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import dev.sdkforge.camera.ui.CameraView +import dev.sdkforge.camera.ui.rememberCameraController + +@Composable +fun ScannerScreen( + modifier: Modifier = Modifier +) { + val cameraController = rememberCameraController() + + ApplicationTheme { + Surface( + modifier = modifier, + color = MaterialTheme.colorScheme.background, + ) { + CameraView( + cameraController = cameraController, + modifier = Modifier.fillMaxSize(), + ) + } + } +} diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index 6de57c4..cf3b176 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -18,6 +18,7 @@ kotlin { implementation(compose.foundation) implementation(compose.material3) + implementation(compose.materialIconsExtended) } } commonTest { diff --git a/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt b/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt index 8046e54..64888ea 100644 --- a/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt +++ b/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt @@ -169,4 +169,20 @@ internal actual class PlatformCameraView( internal actual fun onRelease() { controller.unbind() } + + internal actual fun toggleFlash() { + TODO("NOT IMPLEMENTED YET") + } + + internal actual fun isFlashIsOn(): Boolean { + TODO("NOT IMPLEMENTED YET") + } + + internal actual fun toggleActiveCamera() { + TODO("NOT IMPLEMENTED YET") + } + + internal actual fun isBackCameraActive(): Boolean { + TODO("NOT IMPLEMENTED YET") + } } diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt index 696d47b..b516cf1 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt @@ -138,6 +138,22 @@ internal abstract class NativeCameraController( override fun onRelease() { platformCameraView.onRelease() } + + override fun toggleFlash() { + platformCameraView.toggleFlash() + } + + override fun isFlashIsOn(): Boolean { + return platformCameraView.isFlashIsOn() + } + + override fun toggleActiveCamera() { + platformCameraView.toggleActiveCamera() + } + + override fun isBackCameraActive(): Boolean { + return platformCameraView.isBackCameraActive() + } } /** @@ -195,4 +211,12 @@ abstract class CameraController { * Implementations should properly clean up camera resources. */ internal abstract fun onRelease() + + internal abstract fun toggleFlash() + + internal abstract fun isFlashIsOn(): Boolean + + internal abstract fun toggleActiveCamera() + + internal abstract fun isBackCameraActive(): Boolean } diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt index c301775..5694eec 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt @@ -46,4 +46,12 @@ internal expect class PlatformCameraView { * properly clean up camera resources and stop the preview. */ internal fun onRelease() + + internal fun toggleFlash() + + internal fun isFlashIsOn(): Boolean + + internal fun toggleActiveCamera() + + internal fun isBackCameraActive(): Boolean } diff --git a/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt b/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt index ca1fd4a..81bf621 100644 --- a/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt +++ b/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt @@ -265,4 +265,20 @@ internal actual class PlatformCameraView( internal actual fun onRelease() { captureSession.stopRunning() } + + internal actual fun toggleFlash() { + TODO("NOT IMPLEMENTED YET") + } + + internal actual fun isFlashIsOn(): Boolean { + TODO("NOT IMPLEMENTED YET") + } + + internal actual fun toggleActiveCamera() { + TODO("NOT IMPLEMENTED YET") + } + + internal actual fun isBackCameraActive(): Boolean { + TODO("NOT IMPLEMENTED YET") + } } From 39bc5ebb0bb86ccb330cd8d3d4010e80627ee623 Mon Sep 17 00:00:00 2001 From: "yevhen.kryvoshei" Date: Tue, 16 Sep 2025 13:02:23 +0300 Subject: [PATCH 2/7] Added implementations for flash and camera functions --- .../camera/ui/PlatformCameraView.android.kt | 17 ++++--- .../sdkforge/camera/ui/CameraController.kt | 6 --- .../sdkforge/camera/ui/PlatformCameraView.kt | 2 - .../camera/ui/PlatformCameraView.ios.kt | 48 ++++++++++++++++--- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt b/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt index 64888ea..21c58e0 100644 --- a/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt +++ b/shared-ui/src/androidMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.android.kt @@ -5,6 +5,7 @@ import android.graphics.Color import androidx.annotation.OptIn import androidx.camera.core.CameraSelector import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.TorchState import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat @@ -171,18 +172,20 @@ internal actual class PlatformCameraView( } internal actual fun toggleFlash() { - TODO("NOT IMPLEMENTED YET") + val currentTorchState = controller.torchState.value + controller.enableTorch(currentTorchState != TorchState.ON) } internal actual fun isFlashIsOn(): Boolean { - TODO("NOT IMPLEMENTED YET") + return controller.torchState.value == TorchState.ON } internal actual fun toggleActiveCamera() { - TODO("NOT IMPLEMENTED YET") - } - - internal actual fun isBackCameraActive(): Boolean { - TODO("NOT IMPLEMENTED YET") + val currentCameraSelector = controller.cameraSelector + controller.cameraSelector = when (currentCameraSelector) { + CameraSelector.DEFAULT_BACK_CAMERA -> CameraSelector.DEFAULT_FRONT_CAMERA + CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA + else -> CameraSelector.DEFAULT_BACK_CAMERA + } } } diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt index b516cf1..744b918 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt @@ -150,10 +150,6 @@ internal abstract class NativeCameraController( override fun toggleActiveCamera() { platformCameraView.toggleActiveCamera() } - - override fun isBackCameraActive(): Boolean { - return platformCameraView.isBackCameraActive() - } } /** @@ -217,6 +213,4 @@ abstract class CameraController { internal abstract fun isFlashIsOn(): Boolean internal abstract fun toggleActiveCamera() - - internal abstract fun isBackCameraActive(): Boolean } diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt index 5694eec..2c07f6e 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt @@ -52,6 +52,4 @@ internal expect class PlatformCameraView { internal fun isFlashIsOn(): Boolean internal fun toggleActiveCamera() - - internal fun isBackCameraActive(): Boolean } diff --git a/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt b/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt index 81bf621..067e5bb 100644 --- a/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt +++ b/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt @@ -11,9 +11,13 @@ import platform.AVFoundation.AVCaptureAutoFocusRangeRestrictionNear import platform.AVFoundation.AVCaptureConnection import platform.AVFoundation.AVCaptureDevice import platform.AVFoundation.AVCaptureDeviceInput +import platform.AVFoundation.AVCaptureDevicePosition import platform.AVFoundation.AVCaptureDevicePositionBack import platform.AVFoundation.AVCaptureDevicePositionFront +import platform.AVFoundation.AVCaptureDevicePositionUnspecified +import platform.AVFoundation.AVCaptureDeviceType import platform.AVFoundation.AVCaptureDeviceTypeBuiltInTripleCamera +import platform.AVFoundation.AVCaptureInput import platform.AVFoundation.AVCaptureMetadataOutput import platform.AVFoundation.AVCaptureMetadataOutputObjectsDelegateProtocol import platform.AVFoundation.AVCaptureOutput @@ -30,7 +34,10 @@ import platform.AVFoundation.automaticallyEnablesLowLightBoostWhenAvailable import platform.AVFoundation.defaultDeviceWithDeviceType import platform.AVFoundation.isAutoFocusRangeRestrictionSupported import platform.AVFoundation.isLowLightBoostSupported +import platform.AVFoundation.isTorchAvailable import platform.AVFoundation.isTorchModeSupported +import platform.AVFoundation.position +import platform.AVFoundation.setFlashMode import platform.AVFoundation.torchMode import platform.CoreGraphics.CGRectZero import platform.QuartzCore.CALayer @@ -163,7 +170,6 @@ internal actual class PlatformCameraView( preferFrontCamera: Boolean, ): AVCaptureDevice? { val preferredPosition = if (preferFrontCamera) AVCaptureDevicePositionFront else AVCaptureDevicePositionBack - return AVCaptureDevice.defaultDeviceWithDeviceType( deviceType = AVCaptureDeviceTypeBuiltInTripleCamera, mediaType = AVMediaTypeVideo, @@ -267,18 +273,46 @@ internal actual class PlatformCameraView( } internal actual fun toggleFlash() { - TODO("NOT IMPLEMENTED YET") + val device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) + if (device?.isTorchAvailable() == true) { + val targetMode = when (device.torchMode) { + AVCaptureTorchModeOn -> AVCaptureTorchModeOff + AVCaptureTorchModeOff -> AVCaptureTorchModeOn + else -> AVCaptureTorchModeAuto + } + device.setFlashMode(targetMode) + } } internal actual fun isFlashIsOn(): Boolean { - TODO("NOT IMPLEMENTED YET") + val device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) + var isFlashOn = false + if (device?.isTorchAvailable() == true) { + isFlashOn = device.torchMode == AVCaptureTorchModeOn + } + return isFlashOn } internal actual fun toggleActiveCamera() { - TODO("NOT IMPLEMENTED YET") - } + val frontCameraDeviceInput = AVCaptureDevice.defaultDeviceWithDeviceType( + AVCaptureDeviceTypeBuiltInTripleCamera, + AVMediaTypeVideo, + AVCaptureDevicePositionFront + ) as? AVCaptureDeviceInput + val backCameraDeviceInput = AVCaptureDevice.defaultDeviceWithDeviceType( + AVCaptureDeviceTypeBuiltInTripleCamera, + AVMediaTypeVideo, + AVCaptureDevicePositionBack + ) as? AVCaptureDeviceInput - internal actual fun isBackCameraActive(): Boolean { - TODO("NOT IMPLEMENTED YET") + captureSession.beginConfiguration() + if (captureSession.inputs.contains(frontCameraDeviceInput)) { + frontCameraDeviceInput?.let { captureSession.removeInput(it) } + backCameraDeviceInput?.let { captureSession.addInput(it) } + } else if (captureSession.inputs.contains(backCameraDeviceInput)) { + backCameraDeviceInput?.let { captureSession.removeInput(it) } + frontCameraDeviceInput?.let { captureSession.addInput(it) } + } + captureSession.commitConfiguration() } } From a3646d0d697135bcb4079e592a9276406b31a406 Mon Sep 17 00:00:00 2001 From: "yevhen.kryvoshei" Date: Tue, 16 Sep 2025 17:49:50 +0300 Subject: [PATCH 3/7] Added calls to flash and camera position functions to ScannerScreen.kt. Added material icons api to app-shared --- app-shared/build.gradle.kts | 1 + .../dev/sdkforge/camera/app/ScannerScreen.kt | 61 +++++++++++++++++-- shared-ui/build.gradle.kts | 1 - .../sdkforge/camera/ui/CameraController.kt | 6 +- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/app-shared/build.gradle.kts b/app-shared/build.gradle.kts index db18eb8..37c6fe3 100644 --- a/app-shared/build.gradle.kts +++ b/app-shared/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { api(compose.foundation) api(compose.material3) + api(compose.materialIconsExtended) } } } diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt index c5d6de3..26710a8 100644 --- a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt @@ -1,28 +1,79 @@ package dev.sdkforge.camera.app +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FlashOff +import androidx.compose.material.icons.filled.FlashOn +import androidx.compose.material.icons.filled.FlipCameraAndroid +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface 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.unit.dp +import dev.sdkforge.camera.ui.CameraController import dev.sdkforge.camera.ui.CameraView import dev.sdkforge.camera.ui.rememberCameraController @Composable fun ScannerScreen( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val cameraController = rememberCameraController() ApplicationTheme { - Surface( - modifier = modifier, - color = MaterialTheme.colorScheme.background, - ) { + Scaffold( + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> CameraView( cameraController = cameraController, modifier = Modifier.fillMaxSize(), ) + ButtonsOverlay( + controller = cameraController, + modifier = modifier.padding(top = innerPadding.calculateTopPadding()), + ) } } } + +@Composable +fun ButtonsOverlay( + controller: CameraController, + modifier: Modifier = Modifier, +) { + var isFlashOn by remember { mutableStateOf(false) } + val targetIcon = if (isFlashOn) Icons.Default.FlashOff else Icons.Default.FlashOn + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth().padding(8.dp), + ) { + Icon( + imageVector = targetIcon, + contentDescription = "Flash toggle", + modifier = Modifier.clickable { + controller.toggleFlash() + isFlashOn = controller.isFlashIsOn() + }, + ) + Icon( + imageVector = Icons.Default.FlipCameraAndroid, + contentDescription = "Flip between front and back active cameras", + modifier = Modifier.clickable { + controller.toggleActiveCamera() + }, + ) + } +} diff --git a/shared-ui/build.gradle.kts b/shared-ui/build.gradle.kts index cf3b176..6de57c4 100644 --- a/shared-ui/build.gradle.kts +++ b/shared-ui/build.gradle.kts @@ -18,7 +18,6 @@ kotlin { implementation(compose.foundation) implementation(compose.material3) - implementation(compose.materialIconsExtended) } } commonTest { diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt index 744b918..0261ad0 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt @@ -208,9 +208,9 @@ abstract class CameraController { */ internal abstract fun onRelease() - internal abstract fun toggleFlash() + abstract fun toggleFlash() - internal abstract fun isFlashIsOn(): Boolean + abstract fun isFlashIsOn(): Boolean - internal abstract fun toggleActiveCamera() + abstract fun toggleActiveCamera() } From 1762f79b6da8e4b2036386b5b840905780b1fa0a Mon Sep 17 00:00:00 2001 From: "yevhen.kryvoshei" Date: Wed, 17 Sep 2025 11:32:33 +0300 Subject: [PATCH 4/7] Fixed scanned results type and added remembering last value for it --- .../kotlin/dev/sdkforge/camera/ui/CameraController.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt index 0261ad0..1f6b9d8 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt @@ -8,6 +8,7 @@ import dev.sdkforge.camera.domain.CameraConfig import dev.sdkforge.camera.domain.ScanResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow /** * Platform-specific implementation of the native camera controller. @@ -59,7 +60,7 @@ internal abstract class NativeCameraController( * This mutable shared flow is used internally to emit scan results * and can be shared with multiple collectors. */ - protected val initialScannedResults: MutableSharedFlow = MutableSharedFlow() + protected val initialScannedResults: MutableSharedFlow = MutableSharedFlow(replay = 1) /** * Public flow of scanned results. @@ -67,7 +68,7 @@ internal abstract class NativeCameraController( * This flow emits [ScanResult] objects whenever a barcode is successfully * scanned and decoded. */ - override val scannedResults: Flow = initialScannedResults + override val scannedResults: SharedFlow = initialScannedResults /** * The initial camera state implementation. From d605589c678474bcd0dea710c515facce8e566e4 Mon Sep 17 00:00:00 2001 From: "yevhen.kryvoshei" Date: Wed, 17 Sep 2025 18:01:09 +0300 Subject: [PATCH 5/7] Added scan history bottom sheet dialog --- .../sdkforge/camera/android/MainActivity.kt | 16 ++- .../kotlin/dev/sdkforge/camera/app/App.kt | 6 +- .../dev/sdkforge/camera/app/ScannerScreen.kt | 134 +++++++++++++----- 3 files changed, 118 insertions(+), 38 deletions(-) diff --git a/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt b/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt index 6608be4..08b0f94 100644 --- a/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt +++ b/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt @@ -11,12 +11,15 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateSetOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import dev.sdkforge.camera.app.App import dev.sdkforge.camera.domain.CameraConfig import dev.sdkforge.camera.domain.Facing import dev.sdkforge.camera.domain.Format +import dev.sdkforge.camera.domain.ScanResult import dev.sdkforge.camera.ui.rememberCameraController class MainActivity : ComponentActivity() { @@ -52,16 +55,25 @@ class MainActivity : ComponentActivity() { cameraFacing = Facing.BACK, ), ) - + val scans = remember { mutableStateSetOf() } App( cameraController = cameraController, + scans = scans, modifier = Modifier .fillMaxSize(), ) LaunchedEffect(Unit) { cameraController.scannedResults.collect { scanResult -> - toast("${scanResult.format.name}; value = ${scanResult.value}") + if (scans.contains(scanResult)) { + toast("Already scanned, checked scans history") + } else { + if (scans.size >= 5) { + scans.remove(scans.first()) + } + scans.add(scanResult) + toast("${scanResult.format.name}; value = ${scanResult.value}") + } } } diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt index 4398be9..bdde8a9 100644 --- a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt @@ -5,20 +5,22 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import dev.sdkforge.camera.domain.ScanResult import dev.sdkforge.camera.ui.CameraController -import dev.sdkforge.camera.ui.CameraView @Composable fun App( cameraController: CameraController, + scans: Set, modifier: Modifier = Modifier, ) = ApplicationTheme { Surface( modifier = modifier, color = MaterialTheme.colorScheme.background, ) { - CameraView( + ScannerScreen( cameraController = cameraController, + scans = scans, modifier = Modifier.fillMaxSize(), ) } diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt index 26710a8..fdcf856 100644 --- a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt @@ -2,18 +2,25 @@ package dev.sdkforge.camera.app import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FlashOff import androidx.compose.material.icons.filled.FlashOn import androidx.compose.material.icons.filled.FlipCameraAndroid +import androidx.compose.material.icons.filled.History +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -22,28 +29,35 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import dev.sdkforge.camera.domain.ScanResult import dev.sdkforge.camera.ui.CameraController import dev.sdkforge.camera.ui.CameraView -import dev.sdkforge.camera.ui.rememberCameraController @Composable fun ScannerScreen( + cameraController: CameraController, + scans: Set, modifier: Modifier = Modifier, ) { - val cameraController = rememberCameraController() + var isHistoryDialogShown by remember { mutableStateOf(false) } - ApplicationTheme { - Scaffold( + Scaffold( + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> + CameraView( + cameraController = cameraController, modifier = Modifier.fillMaxSize(), - ) { innerPadding -> - CameraView( - cameraController = cameraController, - modifier = Modifier.fillMaxSize(), - ) - ButtonsOverlay( - controller = cameraController, - modifier = modifier.padding(top = innerPadding.calculateTopPadding()), - ) + ) + ButtonsOverlay( + controller = cameraController, + modifier = modifier.padding(top = innerPadding.calculateTopPadding()), + { isHistoryDialogShown = !isHistoryDialogShown }, + ) + ScansHistoryDialog( + showDialog = isHistoryDialogShown, + scans = scans, + ) { + isHistoryDialogShown = false } } } @@ -52,28 +66,80 @@ fun ScannerScreen( fun ButtonsOverlay( controller: CameraController, modifier: Modifier = Modifier, + onHistoryClicked: () -> Unit, ) { var isFlashOn by remember { mutableStateOf(false) } val targetIcon = if (isFlashOn) Icons.Default.FlashOff else Icons.Default.FlashOn - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.fillMaxWidth().padding(8.dp), - ) { - Icon( - imageVector = targetIcon, - contentDescription = "Flash toggle", - modifier = Modifier.clickable { - controller.toggleFlash() - isFlashOn = controller.isFlashIsOn() - }, - ) - Icon( - imageVector = Icons.Default.FlipCameraAndroid, - contentDescription = "Flip between front and back active cameras", - modifier = Modifier.clickable { - controller.toggleActiveCamera() + + Column { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.Top, + modifier = modifier.fillMaxWidth(), + ) { + Icon( + imageVector = Icons.Default.History, + contentDescription = "Show scans history", + modifier = Modifier.clickable { + onHistoryClicked.invoke() + }, + ) + Icon( + imageVector = targetIcon, + contentDescription = "Flash toggle", + modifier = Modifier.clickable { + controller.toggleFlash() + isFlashOn = controller.isFlashIsOn() + }, + ) + Icon( + imageVector = Icons.Default.FlipCameraAndroid, + contentDescription = "Flip between front and back active cameras", + modifier = Modifier.clickable { + controller.toggleActiveCamera() + }, + ) + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun ScansHistoryDialog( + showDialog: Boolean, + scans: Set, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState() + if (showDialog) { + ModalBottomSheet( + onDismissRequest = { + onDismiss.invoke() }, - ) + sheetState = sheetState, + modifier = Modifier.fillMaxSize() + ) { + if (scans.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Scans history is empty", + ) + } + } else { + LazyColumn( + modifier = Modifier.padding(8.dp).fillMaxWidth(), + ) { + itemsIndexed(items = scans.toList()) { index, item -> + Text( + text = "${index + 1}. ${item.value}", + modifier = Modifier.padding(top = 12.dp), + ) + } + } + } + } } } From a5be91bb0ac396b9a299b30b93842c25a12f7526 Mon Sep 17 00:00:00 2001 From: "yevhen.kryvoshei" Date: Fri, 19 Sep 2025 14:29:40 +0300 Subject: [PATCH 6/7] Added scans history dialog. --- .../sdkforge/camera/android/MainActivity.kt | 7 ++++--- .../kotlin/dev/sdkforge/camera/app/App.kt | 8 ++++++++ .../dev/sdkforge/camera/app/ScannerScreen.kt | 5 ++--- .../camera/app/ComposeAppViewController.kt | 19 ++++++++++++++++--- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt b/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt index 08b0f94..5f907ac 100644 --- a/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt +++ b/app-android/src/main/java/dev/sdkforge/camera/android/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import dev.sdkforge.camera.app.App +import dev.sdkforge.camera.app.AppConstants import dev.sdkforge.camera.domain.CameraConfig import dev.sdkforge.camera.domain.Facing import dev.sdkforge.camera.domain.Format @@ -66,13 +67,13 @@ class MainActivity : ComponentActivity() { LaunchedEffect(Unit) { cameraController.scannedResults.collect { scanResult -> if (scans.contains(scanResult)) { - toast("Already scanned, checked scans history") + toast("${AppConstants.SUCCESS_TITLE} - ${AppConstants.ALREADY_SCANNED_MESSAGE}") } else { - if (scans.size >= 5) { + if (scans.size == AppConstants.HISTORY_SCANS_MAX_LENGTH) { scans.remove(scans.first()) } scans.add(scanResult) - toast("${scanResult.format.name}; value = ${scanResult.value}") + toast("${AppConstants.SUCCESS_TITLE} - ${AppConstants.SCANNED_VALUE} ${scanResult.value}") } } } diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt index bdde8a9..6be61eb 100644 --- a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt @@ -25,3 +25,11 @@ fun App( ) } } + +object AppConstants { + const val HISTORY_SCANS_MAX_LENGTH = 5 + const val ERROR_TITLE = "Error" + const val SUCCESS_TITLE = "Success" + const val ALREADY_SCANNED_MESSAGE = "Already scanned, check scans history" + const val SCANNED_VALUE = "Scanned value:" +} diff --git a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt index fdcf856..02ce7b5 100644 --- a/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt +++ b/app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/ScannerScreen.kt @@ -6,7 +6,6 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -117,12 +116,12 @@ fun ScansHistoryDialog( onDismiss.invoke() }, sheetState = sheetState, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { if (scans.isEmpty()) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { Text( text = "Scans history is empty", diff --git a/app-shared/src/iosMain/kotlin/dev/sdkforge/camera/app/ComposeAppViewController.kt b/app-shared/src/iosMain/kotlin/dev/sdkforge/camera/app/ComposeAppViewController.kt index b1863f3..9e77798 100644 --- a/app-shared/src/iosMain/kotlin/dev/sdkforge/camera/app/ComposeAppViewController.kt +++ b/app-shared/src/iosMain/kotlin/dev/sdkforge/camera/app/ComposeAppViewController.kt @@ -2,12 +2,15 @@ package dev.sdkforge.camera.app import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateSetOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.uikit.LocalUIViewController import androidx.compose.ui.window.ComposeUIViewController import dev.sdkforge.camera.domain.CameraConfig import dev.sdkforge.camera.domain.Facing import dev.sdkforge.camera.domain.Format +import dev.sdkforge.camera.domain.ScanResult import dev.sdkforge.camera.ui.rememberCameraController import kotlin.experimental.ExperimentalObjCName import platform.AVFoundation.AVCaptureDevice @@ -37,18 +40,28 @@ fun ComposeAppViewController() = ComposeUIViewController( cameraFacing = Facing.BACK, ), ) - + val scans = remember { mutableStateSetOf() } App( cameraController = cameraController, + scans = scans, modifier = Modifier .fillMaxSize(), ) LaunchedEffect(Unit) { cameraController.scannedResults.collect { scanResult -> + val alertText = if (scans.contains(scanResult)) { + AppConstants.ERROR_TITLE to AppConstants.ALREADY_SCANNED_MESSAGE + } else { + if (scans.size == AppConstants.HISTORY_SCANS_MAX_LENGTH) { + scans.remove(scans.first()) + } + scans.add(scanResult) + AppConstants.SUCCESS_TITLE to "${AppConstants.SCANNED_VALUE} ${scanResult.value}" + } val alert = UIAlertController.alertControllerWithTitle( - title = "Scanned format: ${scanResult.format}", - message = "Scanned value: ${scanResult.value}", + title = alertText.first, + message = alertText.second, preferredStyle = UIAlertControllerStyleAlert, ).apply { addAction( From 8f34956dd3d5c3fe746a80ecd7dee46e1e8245cf Mon Sep 17 00:00:00 2001 From: "yevhen.kryvoshei" Date: Fri, 19 Sep 2025 14:46:18 +0300 Subject: [PATCH 7/7] Added docs for new functions --- .../dev/sdkforge/camera/ui/CameraController.kt | 13 +++++++++++++ .../dev/sdkforge/camera/ui/PlatformCameraView.kt | 13 +++++++++++++ .../sdkforge/camera/ui/PlatformCameraView.ios.kt | 13 +++++++++++++ 3 files changed, 39 insertions(+) diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt index 1f6b9d8..5828428 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/CameraController.kt @@ -209,9 +209,22 @@ abstract class CameraController { */ internal abstract fun onRelease() + /** + * Changes state of camera flash to opposite of current. + * + * Provides control of flash in torch mode only. + */ abstract fun toggleFlash() + /** + * Check for flash is currently on in torch mode. + */ abstract fun isFlashIsOn(): Boolean + /** + * Changes what camera is active at the moment. + * + * Provides control of what camera, frontal or back, is currently active. + */ abstract fun toggleActiveCamera() } diff --git a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt index 2c07f6e..40ef1c7 100644 --- a/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt +++ b/shared-ui/src/commonMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.kt @@ -47,9 +47,22 @@ internal expect class PlatformCameraView { */ internal fun onRelease() + /** + * Changes state of camera flash to opposite of current. + * + * Provides control of flash in torch mode only. + */ internal fun toggleFlash() + /** + * Check for flash is currently on in torch mode. + */ internal fun isFlashIsOn(): Boolean + /** + * Changes what camera is active at the moment. + * + * Provides control of what camera, frontal or back, is currently active. + */ internal fun toggleActiveCamera() } diff --git a/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt b/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt index 067e5bb..4e7019f 100644 --- a/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt +++ b/shared-ui/src/iosMain/kotlin/dev/sdkforge/camera/ui/PlatformCameraView.ios.kt @@ -272,6 +272,11 @@ internal actual class PlatformCameraView( captureSession.stopRunning() } + /** + * Changes state of camera flash to opposite of current. + * + * Provides control of flash in torch mode only. + */ internal actual fun toggleFlash() { val device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) if (device?.isTorchAvailable() == true) { @@ -284,6 +289,9 @@ internal actual class PlatformCameraView( } } + /** + * Check for flash is currently on in torch mode. + */ internal actual fun isFlashIsOn(): Boolean { val device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) var isFlashOn = false @@ -293,6 +301,11 @@ internal actual class PlatformCameraView( return isFlashOn } + /** + * Changes what camera is active at the moment. + * + * Provides control of what camera, frontal or back, is currently active. + */ internal actual fun toggleActiveCamera() { val frontCameraDeviceInput = AVCaptureDevice.defaultDeviceWithDeviceType( AVCaptureDeviceTypeBuiltInTripleCamera,