Skip to content

Commit 120c8dd

Browse files
committed
init repository structure
0 parents  commit 120c8dd

File tree

17 files changed

+1209
-0
lines changed

17 files changed

+1209
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Update Dependencies
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "0 4 * * *"
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout to push branch
13+
uses: actions/checkout@v4
14+
with:
15+
ref: ${{ github.ref }}
16+
fetch-depth: 0
17+
18+
- name: Set up JDK 17
19+
uses: actions/setup-java@v3
20+
with:
21+
java-version: 17
22+
distribution: 'corretto'
23+
24+
- name: Update dependencies
25+
run: |
26+
export GRADLE_USER_HOME=$(pwd)/.gradle
27+
chmod +x ./gradlew
28+
./gradlew versionCatalogUpdate
29+
30+
- name: Create report env variable
31+
run: |
32+
{
33+
echo 'REPORT<<EOF'
34+
cat build/dependencyUpdates/report.txt | tail -n +6
35+
echo EOF
36+
} >> "$GITHUB_ENV"
37+
38+
- name: Create pull request
39+
uses: peter-evans/create-pull-request@v4
40+
with:
41+
token: ${{ secrets.PAT }}
42+
commit-message: update dependencies
43+
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
44+
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
45+
signoff: false
46+
branch: github-actions-bot/update-deps
47+
delete-branch: true
48+
title: 'Update Dependencies'
49+
body: ${{ env.REPORT }}
50+
reviewers: ${{ github.actor }}

.gitignore

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
.gradle
2+
build/
3+
!gradle/wrapper/gradle-wrapper.jar
4+
!**/src/main/**/build/
5+
!**/src/test/**/build/
6+
7+
### IntelliJ IDEA ###
8+
.idea/modules.xml
9+
.idea/jarRepositories.xml
10+
.idea/compiler.xml
11+
.idea/libraries/
12+
*.iws
13+
*.iml
14+
*.ipr
15+
out/
16+
!**/src/main/**/out/
17+
!**/src/test/**/out/
18+
19+
### Eclipse ###
20+
.apt_generated
21+
.classpath
22+
.factorypath
23+
.project
24+
.settings
25+
.springBeans
26+
.sts4-cache
27+
bin/
28+
!**/src/main/**/bin/
29+
!**/src/test/**/bin/
30+
31+
### NetBeans ###
32+
/nbproject/private/
33+
/nbbuild/
34+
/dist/
35+
/nbdist/
36+
/.nb-gradle/
37+
38+
### VS Code ###
39+
.vscode/
40+
41+
### Mac OS ###
42+
.DS_Store
43+
44+
local.properties

build.gradle.kts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
2+
3+
plugins {
4+
alias(libs.plugins.kotlin.multiplatform) apply false
5+
alias(libs.plugins.android.library) apply false
6+
alias(libs.plugins.compose.multiplatform) apply false
7+
8+
alias(libs.plugins.gradle.versions) apply true
9+
alias(libs.plugins.version.catalog.update) apply true
10+
}
11+
12+
versionCatalogUpdate {
13+
sortByKey = true
14+
}
15+
16+
fun isStable(version: String): Boolean {
17+
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
18+
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
19+
return stableKeyword || regex.matches(version)
20+
}
21+
22+
tasks.withType<DependencyUpdatesTask> {
23+
rejectVersionIf {
24+
!isStable(candidate.version)
25+
}
26+
}

camera-qr/build.gradle.kts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
plugins {
2+
alias(libs.plugins.kotlin.multiplatform) apply true
3+
alias(libs.plugins.android.library) apply true
4+
alias(libs.plugins.compose.multiplatform) apply true
5+
id("maven-publish") apply true
6+
}
7+
8+
repositories {
9+
mavenCentral()
10+
google()
11+
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
12+
}
13+
14+
group = "dev.procyk.compose"
15+
version = libs.versions.compose.extensions.get()
16+
17+
kotlin {
18+
jvm("desktop")
19+
20+
androidTarget {
21+
publishLibraryVariants("release")
22+
}
23+
ios()
24+
iosSimulatorArm64()
25+
26+
sourceSets {
27+
val commonMain by getting {
28+
dependencies {
29+
implementation(compose.runtime)
30+
implementation(compose.foundation)
31+
}
32+
}
33+
val androidMain by getting {
34+
dependencies {
35+
implementation(libs.androidx.camera)
36+
implementation(libs.androidx.cameraLifecycle)
37+
implementation(libs.androidx.cameraPreview)
38+
39+
implementation(libs.mlkit.barcodeScanning)
40+
41+
implementation(libs.accompanist.permissions)
42+
}
43+
}
44+
val iosX64Main by getting
45+
val iosArm64Main by getting
46+
val iosSimulatorArm64Main by getting
47+
val iosMain by getting {
48+
dependsOn(commonMain)
49+
50+
iosX64Main.dependsOn(this)
51+
iosArm64Main.dependsOn(this)
52+
iosSimulatorArm64Main.dependsOn(this)
53+
}
54+
val desktopMain by getting {
55+
dependencies {
56+
implementation(libs.webcam.capture)
57+
implementation(libs.webcam.capture.driver.native)
58+
implementation(libs.zxing.javase)
59+
}
60+
}
61+
}
62+
}
63+
64+
android {
65+
compileSdk = libs.versions.android.compileSdk.get().toInt()
66+
namespace = "dev.procyk.compose"
67+
68+
defaultConfig {
69+
minSdk = libs.versions.android.minSdk.get().toInt()
70+
}
71+
compileOptions {
72+
sourceCompatibility = JavaVersion.VERSION_17
73+
targetCompatibility = JavaVersion.VERSION_17
74+
}
75+
kotlin {
76+
jvmToolchain(17)
77+
}
78+
}
79+
80+
publishing {
81+
repositories {
82+
mavenLocal()
83+
}
84+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package dev.procyk.compose.camera.qr
2+
3+
import android.media.Image
4+
import android.widget.LinearLayout.LayoutParams
5+
import android.widget.ListPopupWindow
6+
import androidx.camera.core.ExperimentalGetImage
7+
import androidx.camera.core.ImageAnalysis
8+
import androidx.camera.core.ImageProxy
9+
import androidx.camera.view.LifecycleCameraController
10+
import androidx.camera.view.PreviewView
11+
import androidx.compose.foundation.layout.fillMaxSize
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.DisposableEffect
14+
import androidx.compose.runtime.remember
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.graphics.Color
17+
import androidx.compose.ui.graphics.toArgb
18+
import androidx.compose.ui.platform.LocalContext
19+
import androidx.compose.ui.platform.LocalLifecycleOwner
20+
import androidx.compose.ui.viewinterop.AndroidView
21+
import com.google.accompanist.permissions.ExperimentalPermissionsApi
22+
import com.google.accompanist.permissions.PermissionStatus
23+
import com.google.accompanist.permissions.rememberPermissionState
24+
import com.google.mlkit.vision.barcode.BarcodeScanner
25+
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
26+
import com.google.mlkit.vision.barcode.BarcodeScanning
27+
import com.google.mlkit.vision.barcode.common.Barcode
28+
import com.google.mlkit.vision.common.InputImage
29+
import java.util.concurrent.ExecutorService
30+
import java.util.concurrent.Executors
31+
32+
@Composable
33+
actual fun QRCodeScanner(
34+
onResult: (QRResult) -> Boolean,
35+
onIsLoadingChange: (Boolean) -> Unit,
36+
backgroundColor: Color,
37+
contentDescription: String?,
38+
missingCameraContent: @Composable () -> Unit,
39+
) {
40+
val localContext = LocalContext.current
41+
val lifecycleOwner = LocalLifecycleOwner.current
42+
val analysisExecutor = rememberExecutor()
43+
val cameraController = remember { LifecycleCameraController(localContext) }
44+
AndroidView(
45+
modifier = Modifier.fillMaxSize(),
46+
factory = { context ->
47+
PreviewView(context).apply {
48+
setBackgroundColor(backgroundColor.toArgb())
49+
50+
layoutParams = LayoutParams(ListPopupWindow.MATCH_PARENT, ListPopupWindow.MATCH_PARENT)
51+
scaleType = PreviewView.ScaleType.FILL_START
52+
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
53+
controller = cameraController
54+
55+
var handleNext = true
56+
57+
cameraController.setImageAnalysisAnalyzer(
58+
analysisExecutor,
59+
QRCodeAnalyzer(
60+
handle = { handleNext = handleNext && onResult(it) },
61+
onPassCompleted = onIsLoadingChange,
62+
)
63+
)
64+
65+
cameraController.bindToLifecycle(lifecycleOwner)
66+
}
67+
},
68+
onRelease = {
69+
cameraController.unbind()
70+
analysisExecutor.shutdown()
71+
}
72+
)
73+
}
74+
75+
@OptIn(ExperimentalPermissionsApi::class)
76+
@Composable
77+
actual fun rememberCameraPermissionState(): CameraPermissionState {
78+
val cameraPermissionState = rememberPermissionState(
79+
android.Manifest.permission.CAMERA
80+
)
81+
return remember {
82+
object : CameraPermissionState {
83+
override val isAvailable: Boolean = true
84+
85+
override val permission: CameraPermission
86+
get() = when (cameraPermissionState.status) {
87+
PermissionStatus.Granted -> CameraPermission.Granted
88+
is PermissionStatus.Denied -> CameraPermission.Denied
89+
}
90+
91+
override fun launchRequest() = cameraPermissionState.launchPermissionRequest()
92+
}
93+
}
94+
}
95+
96+
@Composable
97+
private fun rememberExecutor(): ExecutorService {
98+
val executor = remember { Executors.newSingleThreadExecutor() }
99+
DisposableEffect(Unit) {
100+
onDispose {
101+
executor.shutdown()
102+
}
103+
}
104+
return executor
105+
}
106+
107+
internal class QRCodeAnalyzer(
108+
private val handle: (QRResult) -> Unit,
109+
private val onPassCompleted: (failureOccurred: Boolean) -> Unit,
110+
) : ImageAnalysis.Analyzer {
111+
112+
private val barcodeScanner: BarcodeScanner? by lazy {
113+
try {
114+
BarcodeScanning.getClient(
115+
BarcodeScannerOptions.Builder()
116+
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
117+
.build()
118+
)
119+
} catch (_: Exception) {
120+
handleSynchronized(QRResult.QRError)
121+
null
122+
}
123+
}
124+
125+
@Volatile
126+
private var failureTimestamp: Long = NO_FAILURE_FLAG
127+
128+
@ExperimentalGetImage
129+
override fun analyze(imageProxy: ImageProxy) {
130+
val image = imageProxy.image ?: return
131+
132+
if (failureTimestamp.isFailure && System.currentTimeMillis() - failureTimestamp < FAILURE_THROTTLE_MILLIS) {
133+
return imageProxy.close()
134+
}
135+
failureTimestamp = NO_FAILURE_FLAG
136+
137+
val scanner = barcodeScanner ?: return
138+
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
139+
scanner
140+
.process(image.toInputImage(rotationDegrees))
141+
.addOnSuccessListener { onNonEmptySuccess(it) }
142+
.addOnFailureListener {
143+
failureTimestamp = System.currentTimeMillis()
144+
handleSynchronized(QRResult.QRError)
145+
}
146+
.addOnCompleteListener {
147+
onPassCompleted(failureTimestamp.isFailure)
148+
imageProxy.close()
149+
}
150+
}
151+
152+
@Synchronized
153+
private fun handleSynchronized(result: QRResult) {
154+
handle(result)
155+
}
156+
157+
private fun onNonEmptySuccess(codes: List<Barcode?>?) {
158+
val qrResult = QRResult.QRSuccess(codes) { it?.rawValue } ?: return
159+
handleSynchronized(qrResult)
160+
}
161+
}
162+
163+
private const val FAILURE_THROTTLE_MILLIS: Long = 1_000L
164+
165+
private const val NO_FAILURE_FLAG: Long = Long.MIN_VALUE
166+
167+
private inline val Long.isFailure: Boolean get() = this != NO_FAILURE_FLAG
168+
169+
@ExperimentalGetImage
170+
private fun Image.toInputImage(rotationDegrees: Int): InputImage =
171+
InputImage.fromMediaImage(this, rotationDegrees)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.procyk.compose.camera
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import kotlinx.coroutines.CoroutineScope
6+
7+
@Composable
8+
internal fun OnceLaunchedEffect(block: suspend CoroutineScope.() -> Unit) {
9+
LaunchedEffect(Unit, block)
10+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package dev.procyk.compose.camera
2+
3+
internal inline fun <T, U> runIfNonNull(t: T?, crossinline action: (T) -> U): U? =
4+
if (t != null) action(t) else null

0 commit comments

Comments
 (0)