Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import dev.ohs.fhir.catalog.ui.questionnaire.components.ErrorStateToggleAction
import dev.ohs.fhir.catalog.views.barcode.BarcodeItemViewFactoryMatcher
import dev.ohs.fhir.datacapture.views.barcode.BarcodeItemViewFactoryMatcher
import dev.ohs.fhir.catalog.views.locationwidget.LocationCaptureItemViewFactoryMatcher
import dev.ohs.fhir.catalog.views.locationwidget.LocationCoordinateItemViewFactoryMatcher
import dev.ohs.fhir.datacapture.Questionnaire
Expand Down
26 changes: 25 additions & 1 deletion datacapture/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,22 @@ kotlin {
}
}

androidMain { resources.srcDir("res") }
androidMain {
resources.srcDir("res")
dependencies {
implementation(libs.kscan)
implementation(libs.moko.permissions.camera)
implementation(libs.moko.permissions.compose)
}
}

iosMain {
dependencies {
implementation(libs.kscan)
implementation(libs.moko.permissions.camera)
implementation(libs.moko.permissions.compose)
}
}

getByName("androidDeviceTest") {
dependencies {
Expand Down Expand Up @@ -137,6 +152,15 @@ kotlin {
implementation(libs.kotlinx.coroutines.swing)
}
}

listOf("iosX64Main", "iosArm64Main", "iosSimulatorArm64Main").forEach {
getByName(it) {
dependencies {
implementation(libs.moko.permissions.camera)
implementation(libs.moko.permissions.compose)
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.ohs.fhir.catalog.views.barcode
package dev.ohs.fhir.datacapture.views.barcode

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
Expand All @@ -24,7 +24,7 @@ import dev.icerock.moko.permissions.compose.BindEffect
import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory

@Composable
internal actual fun rememberCameraPermissionProvider(): CameraPermissionProvider {
actual fun rememberCameraPermissionProvider(): CameraPermissionProvider {
val lifecycleOwner = LocalLifecycleOwner.current
val factory = rememberPermissionsControllerFactory()
val controller = remember(factory) { factory.createPermissionsController() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2026 Open Health Stack Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.ohs.fhir.datacapture.views.barcode

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.ncgroup.kscan.BarcodeFormat
import org.ncgroup.kscan.BarcodeResult
import org.ncgroup.kscan.ScannerView

@Composable
actual fun ScannerDialog(onDismiss: () -> Unit, onBarcode: (String?) -> Unit) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Surface(modifier = Modifier.fillMaxSize()) {
ScannerView(codeTypes = listOf(BarcodeFormat.FORMAT_ALL_FORMATS)) { result ->
when (result) {
is BarcodeResult.OnSuccess -> {
onBarcode(result.barcode.data)
onDismiss()
}

is BarcodeResult.OnFailed -> {
result.exception.printStackTrace()
onBarcode(null)
onDismiss()
}

is BarcodeResult.OnCanceled -> {
onBarcode(null)
onDismiss()
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<vector
android:height="21dp"
android:viewportHeight="16"
android:viewportWidth="21"
android:width="27.5625dp"
xmlns:android="http://schemas.android.com/apk/res/android"
>
<path
android:fillColor="#00000000"
android:pathData="M4.5,3.7144L4.5,12.2858"
android:strokeColor="#6C737A"
android:strokeWidth="1"
/>
<path
android:fillColor="#00000000"
android:pathData="M8.5,3.7144L8.5,12.2858"
android:strokeColor="#6C737A"
android:strokeWidth="1"
/>
<path
android:fillColor="#00000000"
android:pathData="M6.5,3.7144L6.5,12.2858"
android:strokeColor="#6C737A"
android:strokeWidth="1"
/>
<path
android:fillColor="#00000000"
android:pathData="M12.5,3.7144L12.5,12.2858"
android:strokeColor="#6C737A"
android:strokeWidth="1"
/>
<path
android:fillColor="#00000000"
android:pathData="M14.5,3.7144L14.5,12.2858"
android:strokeColor="#6C737A"
android:strokeWidth="1"
/>
<path
android:fillColor="#00000000"
android:pathData="M16.5,3.7144L16.5,12.2858"
android:strokeColor="#6C737A"
android:strokeWidth="1"
/>
<path
android:fillColor="#00000000"
android:pathData="M10.5,3.7144L10.5,12.2858"
android:strokeColor="#6C737A"
android:strokeWidth="1"
/>
<path
android:fillColor="#00000000"
android:pathData="M3,1.25L18,1.25A1.75,1.75 0,0 1,19.75 3L19.75,13A1.75,1.75 0,0 1,18 14.75L3,14.75A1.75,1.75 0,0 1,1.25 13L1.25,3A1.75,1.75 0,0 1,3 1.25z"
android:strokeColor="#6C737A"
android:strokeWidth="1.5"
/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,9 @@
<string name="space_asterisk">\u0020\u002a</string>
<string name="repeated_group_title">%1$s. %2$s</string>
<string name="add_repeated_group_item">Add %1$s</string>
<string name="scan_barcode">SCAN BARCODE</string>
<string name="rescan">RE-SCAN</string>
<string name="camera_permission_required">Camera Permission Required</string>
<string name="camera_permission_message">Camera access is required to scan barcodes. Please grant camera permission to continue.</string>
<string name="open_settings">Open Settings</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.ohs.fhir.catalog.views.barcode
package dev.ohs.fhir.datacapture.views.barcode

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
Expand All @@ -42,8 +40,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import dev.ohs.fhir.datacapture.QuestionnaireItemViewFactoryMatcher
import dev.ohs.fhir.datacapture.extensions.itemControlCode
import dev.ohs.fhir.datacapture.theme.QuestionnaireTheme
Expand All @@ -55,22 +51,19 @@ import dev.ohs.fhir.model.r4.Enumeration
import dev.ohs.fhir.model.r4.Questionnaire
import dev.ohs.fhir.model.r4.QuestionnaireResponse
import dev.ohs.fhir.model.r4.String
import kotlin_fhir_data_capture.catalog.generated.resources.Res
import kotlin_fhir_data_capture.catalog.generated.resources.camera_permission_message
import kotlin_fhir_data_capture.catalog.generated.resources.camera_permission_required
import kotlin_fhir_data_capture.catalog.generated.resources.cancel
import kotlin_fhir_data_capture.catalog.generated.resources.ic_barcode
import kotlin_fhir_data_capture.catalog.generated.resources.open_settings
import kotlin_fhir_data_capture.catalog.generated.resources.rescan
import kotlin_fhir_data_capture.catalog.generated.resources.scan_barcode
import kotlin_fhir_data_capture.datacapture.generated.resources.Res
import kotlin_fhir_data_capture.datacapture.generated.resources.camera_permission_message
import kotlin_fhir_data_capture.datacapture.generated.resources.camera_permission_required
import kotlin_fhir_data_capture.datacapture.generated.resources.cancel
import kotlin_fhir_data_capture.datacapture.generated.resources.ic_barcode
import kotlin_fhir_data_capture.datacapture.generated.resources.open_settings
import kotlin_fhir_data_capture.datacapture.generated.resources.rescan
import kotlin_fhir_data_capture.datacapture.generated.resources.scan_barcode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.ncgroup.kscan.BarcodeFormat
import org.ncgroup.kscan.BarcodeResult
import org.ncgroup.kscan.ScannerView

internal object BarcodeItemViewFactory : QuestionnaireItemViewFactory {

Expand Down Expand Up @@ -165,48 +158,25 @@ internal object BarcodeItemViewFactory : QuestionnaireItemViewFactory {
}

if (showScanner) {
ScannerViewDialog(onDismiss = { showScanner = false }) { result ->
coroutineScope.launch {
when (result) {
is BarcodeResult.OnSuccess -> {
val barcode = result.barcode.data
if (barcode.isBlank()) {
questionnaireViewItem.clearAnswer()
} else {
questionnaireViewItem.setAnswer(
QuestionnaireResponse.Item.Answer(
value =
QuestionnaireResponse.Item.Answer.Value.String(
value = String(value = barcode)
)
)
ScannerDialog(
onDismiss = { showScanner = false },
onBarcode = { barcode ->
coroutineScope.launch {
if (barcode.isNullOrBlank()) {
questionnaireViewItem.clearAnswer()
} else {
questionnaireViewItem.setAnswer(
QuestionnaireResponse.Item.Answer(
value =
QuestionnaireResponse.Item.Answer.Value.String(
value = String(value = barcode)
)
)
}
}

is BarcodeResult.OnFailed -> {
result.exception.printStackTrace()
)
}

is BarcodeResult.OnCanceled -> {} // user canceled scanning, nothing to do
}
}
}
}
}
}

@Composable
fun ScannerViewDialog(onDismiss: () -> Unit, onBarcodeResult: (BarcodeResult) -> Unit) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Surface(modifier = Modifier.fillMaxSize()) {
ScannerView(codeTypes = listOf(BarcodeFormat.FORMAT_ALL_FORMATS)) { result ->
onBarcodeResult(result)
onDismiss()
}
},
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.ohs.fhir.catalog.views.barcode
package dev.ohs.fhir.datacapture.views.barcode

import androidx.compose.runtime.Composable

internal interface CameraPermissionProvider {
interface CameraPermissionProvider {
suspend fun providePermission()

fun openSettings()
}

@Composable internal expect fun rememberCameraPermissionProvider(): CameraPermissionProvider
@Composable expect fun rememberCameraPermissionProvider(): CameraPermissionProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2026 Open Health Stack Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.ohs.fhir.datacapture.views.barcode

import androidx.compose.runtime.Composable

@Composable expect fun ScannerDialog(onDismiss: () -> Unit, onBarcode: (String?) -> Unit)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.ohs.fhir.catalog.views.barcode
package dev.ohs.fhir.datacapture.views.barcode

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
Expand All @@ -24,7 +24,7 @@ import dev.icerock.moko.permissions.compose.BindEffect
import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory

@Composable
internal actual fun rememberCameraPermissionProvider(): CameraPermissionProvider {
actual fun rememberCameraPermissionProvider(): CameraPermissionProvider {
val lifecycleOwner = LocalLifecycleOwner.current
val factory = rememberPermissionsControllerFactory()
val controller = remember(factory) { factory.createPermissionsController() }
Expand Down
Loading
Loading