flashlight icon + bounding boxes
This commit is contained in:
@@ -17,7 +17,8 @@ Offline-first, ad-free QR/barcode scanner built with Kotlin, Jetpack Compose, Ca
|
|||||||
|
|
||||||
## MVP Features
|
## MVP Features
|
||||||
- Home: Scan-Button, lokaler Historie-Toggle (Default: OFF), Datenschutz-Dialog
|
- Home: Scan-Button, lokaler Historie-Toggle (Default: OFF), Datenschutz-Dialog
|
||||||
- Scanner: CameraX Live-Preview, Fadenkreuz-Overlay, Taschenlampe, Debounce gegen Doppelscans
|
- Scanner: CameraX Live-Preview, Fadenkreuz-Overlay, Taschenlampe, Debounce gegen Doppelscans, Live-Hinweise zu erkannten/lesbaren Codes
|
||||||
|
- Bild-Scan: Multi-Code-Erkennung aus einem Bild mit Ergebnis-Auswahl
|
||||||
- Ergebnis-Bottom-Sheet: Typ/Inhalt + Copy/Share/Open/Scan again
|
- Ergebnis-Bottom-Sheet: Typ/Inhalt + Copy/Share/Open/Scan again
|
||||||
- URL-Sicherheitswarnung bei lokalem `riskScore >= 3` (kein Blocken, nur Hinweis)
|
- URL-Sicherheitswarnung bei lokalem `riskScore >= 3` (kein Blocken, nur Hinweis)
|
||||||
- Historie: Suche, Swipe-to-delete, Alles-löschen, Detailansicht mit Volltext
|
- Historie: Suche, Swipe-to-delete, Alles-löschen, Detailansicht mit Volltext
|
||||||
|
|||||||
+7
-4
@@ -18,6 +18,9 @@
|
|||||||
- [x] Settings for scan feedback
|
- [x] Settings for scan feedback
|
||||||
- Toggle haptic/beep on successful scan.
|
- Toggle haptic/beep on successful scan.
|
||||||
|
|
||||||
|
- [x] Live readability feedback in camera
|
||||||
|
- Show real-time hint when a code is visible vs readable.
|
||||||
|
|
||||||
## Mid-size Features (3-7 days)
|
## Mid-size Features (3-7 days)
|
||||||
|
|
||||||
1. Import history
|
1. Import history
|
||||||
@@ -30,8 +33,8 @@
|
|||||||
3. Tagging
|
3. Tagging
|
||||||
- Add tags/folders and filter chips in History.
|
- Add tags/folders and filter chips in History.
|
||||||
|
|
||||||
4. Improved image scan
|
4. [x] Improved image scan
|
||||||
- Support multi-code detection from one image and let user pick result.
|
- Multi-code detection from one image with result picker.
|
||||||
|
|
||||||
## Bigger Features (1-3 weeks)
|
## Bigger Features (1-3 weeks)
|
||||||
|
|
||||||
@@ -50,5 +53,5 @@
|
|||||||
|
|
||||||
## Suggested Next 2
|
## Suggested Next 2
|
||||||
|
|
||||||
1. Banner/snackbar duplicate feedback
|
1. Import history (CSV/JSON restore + merge policy)
|
||||||
2. CSV/JSON export in History
|
2. Favorites / pin scans
|
||||||
|
|||||||
@@ -1,23 +1,71 @@
|
|||||||
package com.clean.scanner.data.scanner
|
package com.clean.scanner.data.scanner
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import com.clean.scanner.domain.ScanResult
|
import com.clean.scanner.domain.ScanResult
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
|
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||||
import com.google.mlkit.vision.barcode.common.Barcode
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.google.mlkit.vision.common.InputImage
|
import com.google.mlkit.vision.common.InputImage
|
||||||
|
|
||||||
|
data class DetectionPoint(
|
||||||
|
val x: Float,
|
||||||
|
val y: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DetectionBox(
|
||||||
|
val left: Float,
|
||||||
|
val top: Float,
|
||||||
|
val right: Float,
|
||||||
|
val bottom: Float,
|
||||||
|
val corners: List<DetectionPoint> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
class MlKitBarcodeAnalyzer(
|
class MlKitBarcodeAnalyzer(
|
||||||
private val isAnalysisEnabled: () -> Boolean = { true },
|
private val isAnalysisEnabled: () -> Boolean = { true },
|
||||||
|
private val onDetectionStateChanged: (
|
||||||
|
hasPotential: Boolean,
|
||||||
|
hasReadable: Boolean,
|
||||||
|
boxes: List<DetectionBox>,
|
||||||
|
sourceWidth: Int,
|
||||||
|
sourceHeight: Int
|
||||||
|
) -> Unit = { _, _, _, _, _ -> },
|
||||||
private val onDetected: (ScanResult) -> Unit
|
private val onDetected: (ScanResult) -> Unit
|
||||||
) : ImageAnalysis.Analyzer, AutoCloseable {
|
) : ImageAnalysis.Analyzer, AutoCloseable {
|
||||||
|
|
||||||
private val scanner = BarcodeScanning.getClient()
|
private val scanner = BarcodeScanning.getClient(
|
||||||
|
BarcodeScannerOptions.Builder()
|
||||||
|
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
|
||||||
|
.enableAllPotentialBarcodes()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
@Volatile
|
@Volatile
|
||||||
private var processing = false
|
private var processing = false
|
||||||
|
@Volatile
|
||||||
|
private var lastHasPotential = false
|
||||||
|
@Volatile
|
||||||
|
private var lastHasReadable = false
|
||||||
|
@Volatile
|
||||||
|
private var lastBoxes: List<DetectionBox> = emptyList()
|
||||||
|
@Volatile
|
||||||
|
private var lastSourceWidth = 0
|
||||||
|
@Volatile
|
||||||
|
private var lastSourceHeight = 0
|
||||||
|
|
||||||
override fun analyze(imageProxy: ImageProxy) {
|
override fun analyze(imageProxy: ImageProxy) {
|
||||||
if (!isAnalysisEnabled() || processing) {
|
if (!isAnalysisEnabled()) {
|
||||||
|
publishDetectionState(
|
||||||
|
hasPotential = false,
|
||||||
|
hasReadable = false,
|
||||||
|
boxes = emptyList(),
|
||||||
|
sourceWidth = 0,
|
||||||
|
sourceHeight = 0
|
||||||
|
)
|
||||||
|
imageProxy.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (processing) {
|
||||||
imageProxy.close()
|
imageProxy.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -26,15 +74,58 @@ class MlKitBarcodeAnalyzer(
|
|||||||
imageProxy.close()
|
imageProxy.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
||||||
|
val sourceWidth = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.height else imageProxy.width
|
||||||
|
val sourceHeight = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.width else imageProxy.height
|
||||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||||
processing = true
|
processing = true
|
||||||
|
|
||||||
scanner.process(image)
|
scanner.process(image)
|
||||||
.addOnSuccessListener { barcodes ->
|
.addOnSuccessListener { barcodes ->
|
||||||
val first = barcodes.firstOrNull() ?: return@addOnSuccessListener
|
val readable = barcodes.firstOrNull { !it.rawValue.isNullOrBlank() }
|
||||||
val raw = first.rawValue ?: return@addOnSuccessListener
|
val boxes = barcodes.mapNotNull { barcode ->
|
||||||
val type = first.valueType.toHumanType()
|
val bounds = barcode.boundingBox ?: return@mapNotNull null
|
||||||
onDetected(ScanResult(content = raw, type = type))
|
val normalized = normalizeBoundingBox(
|
||||||
|
rect = bounds,
|
||||||
|
width = imageProxy.width,
|
||||||
|
height = imageProxy.height,
|
||||||
|
rotationDegrees = rotationDegrees
|
||||||
|
) ?: return@mapNotNull null
|
||||||
|
val normalizedCorners = barcode.cornerPoints?.map { p ->
|
||||||
|
normalizePoint(
|
||||||
|
x = p.x.toFloat(),
|
||||||
|
y = p.y.toFloat(),
|
||||||
|
width = imageProxy.width,
|
||||||
|
height = imageProxy.height,
|
||||||
|
rotationDegrees = rotationDegrees
|
||||||
|
)
|
||||||
|
}?.filterNotNull() ?: emptyList()
|
||||||
|
normalized.copy(corners = normalizedCorners)
|
||||||
|
}
|
||||||
|
publishDetectionState(
|
||||||
|
hasPotential = barcodes.isNotEmpty(),
|
||||||
|
hasReadable = readable != null,
|
||||||
|
boxes = boxes,
|
||||||
|
sourceWidth = sourceWidth,
|
||||||
|
sourceHeight = sourceHeight
|
||||||
|
)
|
||||||
|
if (readable != null) {
|
||||||
|
onDetected(
|
||||||
|
ScanResult(
|
||||||
|
content = readable.rawValue.orEmpty(),
|
||||||
|
type = readable.valueType.toHumanType()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
publishDetectionState(
|
||||||
|
hasPotential = false,
|
||||||
|
hasReadable = false,
|
||||||
|
boxes = emptyList(),
|
||||||
|
sourceWidth = 0,
|
||||||
|
sourceHeight = 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.addOnCompleteListener {
|
.addOnCompleteListener {
|
||||||
processing = false
|
processing = false
|
||||||
@@ -61,4 +152,119 @@ class MlKitBarcodeAnalyzer(
|
|||||||
override fun close() {
|
override fun close() {
|
||||||
scanner.close()
|
scanner.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun publishDetectionState(
|
||||||
|
hasPotential: Boolean,
|
||||||
|
hasReadable: Boolean,
|
||||||
|
boxes: List<DetectionBox>,
|
||||||
|
sourceWidth: Int,
|
||||||
|
sourceHeight: Int
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
lastHasPotential == hasPotential &&
|
||||||
|
lastHasReadable == hasReadable &&
|
||||||
|
lastBoxes == boxes &&
|
||||||
|
lastSourceWidth == sourceWidth &&
|
||||||
|
lastSourceHeight == sourceHeight
|
||||||
|
) return
|
||||||
|
lastHasPotential = hasPotential
|
||||||
|
lastHasReadable = hasReadable
|
||||||
|
lastBoxes = boxes
|
||||||
|
lastSourceWidth = sourceWidth
|
||||||
|
lastSourceHeight = sourceHeight
|
||||||
|
onDetectionStateChanged(hasPotential, hasReadable, boxes, sourceWidth, sourceHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeBoundingBox(
|
||||||
|
rect: Rect,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
rotationDegrees: Int
|
||||||
|
): DetectionBox? {
|
||||||
|
if (width <= 0 || height <= 0) return null
|
||||||
|
|
||||||
|
var left = rect.left.toFloat()
|
||||||
|
var top = rect.top.toFloat()
|
||||||
|
var right = rect.right.toFloat()
|
||||||
|
var bottom = rect.bottom.toFloat()
|
||||||
|
var outWidth = width.toFloat()
|
||||||
|
var outHeight = height.toFloat()
|
||||||
|
|
||||||
|
when (rotationDegrees) {
|
||||||
|
90 -> {
|
||||||
|
left = height - rect.bottom.toFloat()
|
||||||
|
top = rect.left.toFloat()
|
||||||
|
right = height - rect.top.toFloat()
|
||||||
|
bottom = rect.right.toFloat()
|
||||||
|
outWidth = height.toFloat()
|
||||||
|
outHeight = width.toFloat()
|
||||||
|
}
|
||||||
|
180 -> {
|
||||||
|
left = width - rect.right.toFloat()
|
||||||
|
top = height - rect.bottom.toFloat()
|
||||||
|
right = width - rect.left.toFloat()
|
||||||
|
bottom = height - rect.top.toFloat()
|
||||||
|
outWidth = width.toFloat()
|
||||||
|
outHeight = height.toFloat()
|
||||||
|
}
|
||||||
|
270 -> {
|
||||||
|
left = rect.top.toFloat()
|
||||||
|
top = width - rect.right.toFloat()
|
||||||
|
right = rect.bottom.toFloat()
|
||||||
|
bottom = width - rect.left.toFloat()
|
||||||
|
outWidth = height.toFloat()
|
||||||
|
outHeight = width.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outWidth <= 0f || outHeight <= 0f) return null
|
||||||
|
return DetectionBox(
|
||||||
|
left = (left / outWidth).coerceIn(0f, 1f),
|
||||||
|
top = (top / outHeight).coerceIn(0f, 1f),
|
||||||
|
right = (right / outWidth).coerceIn(0f, 1f),
|
||||||
|
bottom = (bottom / outHeight).coerceIn(0f, 1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizePoint(
|
||||||
|
x: Float,
|
||||||
|
y: Float,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
rotationDegrees: Int
|
||||||
|
): DetectionPoint? {
|
||||||
|
if (width <= 0 || height <= 0) return null
|
||||||
|
|
||||||
|
var outX = x
|
||||||
|
var outY = y
|
||||||
|
var outWidth = width.toFloat()
|
||||||
|
var outHeight = height.toFloat()
|
||||||
|
|
||||||
|
when (rotationDegrees) {
|
||||||
|
90 -> {
|
||||||
|
outX = height - y
|
||||||
|
outY = x
|
||||||
|
outWidth = height.toFloat()
|
||||||
|
outHeight = width.toFloat()
|
||||||
|
}
|
||||||
|
180 -> {
|
||||||
|
outX = width - x
|
||||||
|
outY = height - y
|
||||||
|
outWidth = width.toFloat()
|
||||||
|
outHeight = height.toFloat()
|
||||||
|
}
|
||||||
|
270 -> {
|
||||||
|
outX = y
|
||||||
|
outY = width - x
|
||||||
|
outWidth = height.toFloat()
|
||||||
|
outHeight = width.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outWidth <= 0f || outHeight <= 0f) return null
|
||||||
|
return DetectionPoint(
|
||||||
|
x = (outX / outWidth).coerceIn(0f, 1f),
|
||||||
|
y = (outY / outHeight).coerceIn(0f, 1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.ScaleGestureDetector
|
|||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.core.UseCaseGroup
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.clean.scanner.data.scanner.DetectionBox
|
||||||
import com.clean.scanner.data.scanner.MlKitBarcodeAnalyzer
|
import com.clean.scanner.data.scanner.MlKitBarcodeAnalyzer
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -32,19 +34,39 @@ fun CameraPreview(
|
|||||||
analysisEnabled: Boolean,
|
analysisEnabled: Boolean,
|
||||||
torchEnabled: Boolean,
|
torchEnabled: Boolean,
|
||||||
onTorchAvailabilityChanged: (Boolean) -> Unit,
|
onTorchAvailabilityChanged: (Boolean) -> Unit,
|
||||||
|
onDetectionStateChanged: (
|
||||||
|
hasPotential: Boolean,
|
||||||
|
hasReadable: Boolean,
|
||||||
|
boxes: List<DetectionBox>,
|
||||||
|
sourceWidth: Int,
|
||||||
|
sourceHeight: Int
|
||||||
|
) -> Unit = { _, _, _, _, _ -> },
|
||||||
onScan: (String, String) -> Unit
|
onScan: (String, String) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
|
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
|
||||||
|
val mainExecutor = remember(context) { ContextCompat.getMainExecutor(context) }
|
||||||
val previewView = remember { PreviewView(context) }
|
val previewView = remember { PreviewView(context) }
|
||||||
val cameraRef = remember { mutableStateOf<androidx.camera.core.Camera?>(null) }
|
val cameraRef = remember { mutableStateOf<androidx.camera.core.Camera?>(null) }
|
||||||
val zoomRatio = remember { mutableStateOf(1f) }
|
val zoomRatio = remember { mutableStateOf(1f) }
|
||||||
val latestAnalysisEnabled = rememberUpdatedState(analysisEnabled)
|
val latestAnalysisEnabled = rememberUpdatedState(analysisEnabled)
|
||||||
val latestOnScan = rememberUpdatedState(onScan)
|
val latestOnScan = rememberUpdatedState(onScan)
|
||||||
|
val latestOnDetectionStateChanged = rememberUpdatedState(onDetectionStateChanged)
|
||||||
val analyzer = remember {
|
val analyzer = remember {
|
||||||
MlKitBarcodeAnalyzer(
|
MlKitBarcodeAnalyzer(
|
||||||
isAnalysisEnabled = { latestAnalysisEnabled.value },
|
isAnalysisEnabled = { latestAnalysisEnabled.value },
|
||||||
|
onDetectionStateChanged = { hasPotential, hasReadable, boxes, sourceWidth, sourceHeight ->
|
||||||
|
mainExecutor.execute {
|
||||||
|
latestOnDetectionStateChanged.value(
|
||||||
|
hasPotential,
|
||||||
|
hasReadable,
|
||||||
|
boxes,
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
onDetected = { result -> latestOnScan.value(result.content, result.type) }
|
onDetected = { result -> latestOnScan.value(result.content, result.type) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -87,7 +109,18 @@ fun CameraPreview(
|
|||||||
setAnalyzer(cameraExecutor, analyzer)
|
setAnalyzer(cameraExecutor, analyzer)
|
||||||
}
|
}
|
||||||
|
|
||||||
val camera = provider.bindToLifecycle(
|
val camera = previewView.viewPort?.let { viewPort ->
|
||||||
|
val group = UseCaseGroup.Builder()
|
||||||
|
.setViewPort(viewPort)
|
||||||
|
.addUseCase(preview)
|
||||||
|
.addUseCase(imageAnalysis)
|
||||||
|
.build()
|
||||||
|
provider.bindToLifecycle(
|
||||||
|
lifecycleOwner,
|
||||||
|
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||||
|
group
|
||||||
|
)
|
||||||
|
} ?: provider.bindToLifecycle(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||||
preview,
|
preview,
|
||||||
@@ -107,6 +140,7 @@ fun CameraPreview(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
factory = {
|
factory = {
|
||||||
previewView.apply {
|
previewView.apply {
|
||||||
|
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||||
setOnTouchListener { _, event ->
|
setOnTouchListener { _, event ->
|
||||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
performClick()
|
performClick()
|
||||||
|
|||||||
@@ -22,20 +22,25 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.FlashOff
|
||||||
|
import androidx.compose.material.icons.filled.FlashOn
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material.icons.filled.ViewModule
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconToggleButton
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -49,6 +54,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -56,10 +62,12 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.clean.scanner.R
|
import com.clean.scanner.R
|
||||||
|
import com.clean.scanner.data.scanner.DetectionBox
|
||||||
import com.clean.scanner.domain.ScanResult
|
import com.clean.scanner.domain.ScanResult
|
||||||
import com.clean.scanner.ui.BatchScanRecord
|
import com.clean.scanner.ui.BatchScanRecord
|
||||||
import com.clean.scanner.ui.components.CameraPreview
|
import com.clean.scanner.ui.components.CameraPreview
|
||||||
@@ -68,10 +76,12 @@ import com.clean.scanner.util.Intents
|
|||||||
import com.clean.scanner.util.ScanContentParsers
|
import com.clean.scanner.util.ScanContentParsers
|
||||||
import com.clean.scanner.util.UrlRiskScorer
|
import com.clean.scanner.util.UrlRiskScorer
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
|
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||||
import com.google.mlkit.vision.barcode.common.Barcode
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.google.mlkit.vision.common.InputImage
|
import com.google.mlkit.vision.common.InputImage
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -110,8 +120,21 @@ fun ScannerScreen(
|
|||||||
var pendingOpenUrl by remember { mutableStateOf<String?>(null) }
|
var pendingOpenUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var showImageScanFailed by remember { mutableStateOf(false) }
|
var showImageScanFailed by remember { mutableStateOf(false) }
|
||||||
var showNoCodeInImage by remember { mutableStateOf(false) }
|
var showNoCodeInImage by remember { mutableStateOf(false) }
|
||||||
|
var imageScanCandidates by remember { mutableStateOf<List<ScanResult>>(emptyList()) }
|
||||||
|
var hasPotentialInView by remember { mutableStateOf(false) }
|
||||||
|
var hasReadableInView by remember { mutableStateOf(false) }
|
||||||
|
var detectionBoxes by remember { mutableStateOf<List<DetectionBox>>(emptyList()) }
|
||||||
|
var detectionSourceWidth by remember { mutableStateOf(0) }
|
||||||
|
var detectionSourceHeight by remember { mutableStateOf(0) }
|
||||||
val activity = context as? Activity
|
val activity = context as? Activity
|
||||||
val imageScanner = remember { BarcodeScanning.getClient() }
|
val imageScanner = remember {
|
||||||
|
BarcodeScanning.getClient(
|
||||||
|
BarcodeScannerOptions.Builder()
|
||||||
|
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
|
||||||
|
.enableAllPotentialBarcodes()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val permissionLauncher = rememberLauncherForActivityResult(
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission()
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
@@ -138,12 +161,21 @@ fun ScannerScreen(
|
|||||||
|
|
||||||
imageScanner.process(image)
|
imageScanner.process(image)
|
||||||
.addOnSuccessListener { barcodes ->
|
.addOnSuccessListener { barcodes ->
|
||||||
val first = barcodes.firstOrNull()
|
val candidates = barcodes.mapNotNull { barcode ->
|
||||||
val raw = first?.rawValue
|
val raw = barcode.rawValue?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
||||||
if (raw.isNullOrBlank()) {
|
ScanResult(content = raw, type = barcode.valueType.toHumanType())
|
||||||
showNoCodeInImage = true
|
}.distinctBy { "${it.type}|${it.content}" }
|
||||||
|
|
||||||
|
when (candidates.size) {
|
||||||
|
0 -> showNoCodeInImage = true
|
||||||
|
1 -> {
|
||||||
|
if (barcodes.size > 1) {
|
||||||
|
imageScanCandidates = candidates
|
||||||
} else {
|
} else {
|
||||||
onScan(ScanResult(content = raw, type = first.valueType.toHumanType()))
|
onScan(candidates.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> imageScanCandidates = candidates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.addOnFailureListener {
|
.addOnFailureListener {
|
||||||
@@ -194,18 +226,80 @@ fun ScannerScreen(
|
|||||||
analysisEnabled = analysisEnabled,
|
analysisEnabled = analysisEnabled,
|
||||||
torchEnabled = torchEnabled,
|
torchEnabled = torchEnabled,
|
||||||
onTorchAvailabilityChanged = { torchAvailable = it },
|
onTorchAvailabilityChanged = { torchAvailable = it },
|
||||||
|
onDetectionStateChanged = { hasPotential, hasReadable, boxes, sourceWidth, sourceHeight ->
|
||||||
|
hasPotentialInView = hasPotential
|
||||||
|
hasReadableInView = hasReadable
|
||||||
|
detectionBoxes = boxes
|
||||||
|
detectionSourceWidth = sourceWidth
|
||||||
|
detectionSourceHeight = sourceHeight
|
||||||
|
},
|
||||||
onScan = { content, type ->
|
onScan = { content, type ->
|
||||||
onScan(ScanResult(content = content, type = type))
|
onScan(ScanResult(content = content, type = type))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (detectionBoxes.isNotEmpty()) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
val sourceW = detectionSourceWidth.toFloat()
|
||||||
|
val sourceH = detectionSourceHeight.toFloat()
|
||||||
|
if (sourceW <= 0f || sourceH <= 0f) return@Canvas
|
||||||
|
|
||||||
|
val scale = max(size.width / sourceW, size.height / sourceH)
|
||||||
|
val scaledW = sourceW * scale
|
||||||
|
val scaledH = sourceH * scale
|
||||||
|
val offsetX = (size.width - scaledW) / 2f
|
||||||
|
val offsetY = (size.height - scaledH) / 2f
|
||||||
|
|
||||||
|
val boxColor = if (hasReadableInView) Color(0xFF4AE3A3) else Color(0xFFFFC857)
|
||||||
|
detectionBoxes.forEach { box ->
|
||||||
|
val left = offsetX + (box.left * sourceW * scale)
|
||||||
|
val top = offsetY + (box.top * sourceH * scale)
|
||||||
|
val right = offsetX + (box.right * sourceW * scale)
|
||||||
|
val bottom = offsetY + (box.bottom * sourceH * scale)
|
||||||
|
val mappedCorners = box.corners.map { p ->
|
||||||
|
androidx.compose.ui.geometry.Offset(
|
||||||
|
x = offsetX + (p.x * sourceW * scale),
|
||||||
|
y = offsetY + (p.y * sourceH * scale)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (mappedCorners.size >= 4) {
|
||||||
|
val outline = Path().apply {
|
||||||
|
moveTo(mappedCorners.first().x, mappedCorners.first().y)
|
||||||
|
mappedCorners.drop(1).forEach { pt -> lineTo(pt.x, pt.y) }
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
drawPath(
|
||||||
|
path = outline,
|
||||||
|
color = boxColor.copy(alpha = 0.96f),
|
||||||
|
style = Stroke(width = 4f)
|
||||||
|
)
|
||||||
|
} else if (right > left && bottom > top) {
|
||||||
|
drawRoundRect(
|
||||||
|
color = boxColor.copy(alpha = 0.95f),
|
||||||
|
topLeft = androidx.compose.ui.geometry.Offset(left, top),
|
||||||
|
size = androidx.compose.ui.geometry.Size(right - left, bottom - top),
|
||||||
|
cornerRadius = CornerRadius(14f, 14f),
|
||||||
|
style = Stroke(width = 4f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.fillMaxWidth(0.7f)
|
.fillMaxWidth(0.7f)
|
||||||
.height(220.dp)
|
.height(220.dp)
|
||||||
) {
|
) {
|
||||||
val guideColor = Color(0xFF7CE6C6)
|
val guideColor = when {
|
||||||
|
hasReadableInView -> Color(0xFF4AE3A3)
|
||||||
|
hasPotentialInView -> Color(0xFFFFC857)
|
||||||
|
else -> Color(0xFF7CE6C6)
|
||||||
|
}
|
||||||
val cx = size.width / 2f
|
val cx = size.width / 2f
|
||||||
val cy = size.height / 2f
|
val cy = size.height / 2f
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
@@ -225,7 +319,11 @@ fun ScannerScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.pinch_to_zoom_hint),
|
text = when {
|
||||||
|
hasReadableInView -> stringResource(R.string.live_readable_detected)
|
||||||
|
hasPotentialInView -> stringResource(R.string.live_potential_detected)
|
||||||
|
else -> stringResource(R.string.pinch_to_zoom_hint)
|
||||||
|
},
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -262,16 +360,21 @@ fun ScannerScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
if (torchAvailable) {
|
if (torchAvailable) {
|
||||||
OverlayToggle(
|
OverlayIconToggle(
|
||||||
checked = torchEnabled,
|
checked = torchEnabled,
|
||||||
onCheckedChange = { torchEnabled = it },
|
onCheckedChange = { torchEnabled = it },
|
||||||
label = stringResource(R.string.flashlight)
|
label = stringResource(R.string.flashlight),
|
||||||
|
checkedImageVector = Icons.Default.FlashOn,
|
||||||
|
uncheckedImageVector = Icons.Default.FlashOff,
|
||||||
|
showLabel = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OverlayToggle(
|
OverlayIconToggle(
|
||||||
checked = batchMode,
|
checked = batchMode,
|
||||||
onCheckedChange = onBatchModeChange,
|
onCheckedChange = onBatchModeChange,
|
||||||
label = stringResource(R.string.batch_mode)
|
label = stringResource(R.string.batch_mode),
|
||||||
|
checkedImageVector = Icons.Default.ViewModule,
|
||||||
|
uncheckedImageVector = Icons.AutoMirrored.Filled.ViewList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +529,56 @@ fun ScannerScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageScanCandidates.isNotEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { imageScanCandidates = emptyList() },
|
||||||
|
title = { Text(stringResource(R.string.image_scan_pick_title, imageScanCandidates.size)) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.image_scan_pick_subtitle),
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
imageScanCandidates.forEach { candidate ->
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onScan(candidate)
|
||||||
|
imageScanCandidates = emptyList()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = candidate.type,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = candidate.content,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { imageScanCandidates = emptyList() }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showImageScanFailed) {
|
if (showImageScanFailed) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showImageScanFailed = false },
|
onDismissRequest = { showImageScanFailed = false },
|
||||||
@@ -441,12 +594,16 @@ fun ScannerScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OverlayToggle(
|
private fun OverlayIconToggle(
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
label: String
|
label: String,
|
||||||
|
checkedImageVector: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
uncheckedImageVector: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
showLabel: Boolean = true
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(
|
||||||
color = Color.Black.copy(alpha = 0.35f),
|
color = Color.Black.copy(alpha = 0.35f),
|
||||||
@@ -454,8 +611,23 @@ private fun OverlayToggle(
|
|||||||
)
|
)
|
||||||
.padding(horizontal = 10.dp, vertical = 8.dp)
|
.padding(horizontal = 10.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = label, color = Color.White)
|
IconToggleButton(
|
||||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (checked) checkedImageVector else uncheckedImageVector,
|
||||||
|
contentDescription = label,
|
||||||
|
tint = if (checked) Color(0xFF4AE3A3) else Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showLabel) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ package com.clean.scanner.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
object Intents {
|
object Intents {
|
||||||
fun openUrl(context: Context, url: String) {
|
fun openUrl(context: Context, url: String) {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
val intent = Intent(Intent.ACTION_VIEW, url.toUri()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivity(context, intent, null)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareText(context: Context, text: String) {
|
fun shareText(context: Context, text: String) {
|
||||||
@@ -24,27 +23,28 @@ object Intents {
|
|||||||
.putExtra(Intent.EXTRA_TEXT, text)
|
.putExtra(Intent.EXTRA_TEXT, text)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
val chooser = Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
val chooser = Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivity(context, chooser, null)
|
context.startActivity(chooser)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dialPhone(context: Context, number: String) {
|
fun dialPhone(context: Context, number: String) {
|
||||||
val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:${number.trim()}"))
|
val intent = Intent(Intent.ACTION_DIAL, "tel:${number.trim()}".toUri())
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivity(context, intent, null)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("SpellCheckingInspection")
|
||||||
fun sendSms(context: Context, number: String, message: String?) {
|
fun sendSms(context: Context, number: String, message: String?) {
|
||||||
val base = number.trim()
|
val base = number.trim()
|
||||||
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:$base"))
|
val intent = Intent(Intent.ACTION_SENDTO, "smsto:$base".toUri())
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
if (!message.isNullOrBlank()) {
|
if (!message.isNullOrBlank()) {
|
||||||
intent.putExtra("sms_body", message)
|
intent.putExtra("sms_body", message)
|
||||||
}
|
}
|
||||||
startActivity(context, intent, null)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendEmail(context: Context, email: String, subject: String?, body: String? = null) {
|
fun sendEmail(context: Context, email: String, subject: String?, body: String? = null) {
|
||||||
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:${email.trim()}"))
|
val intent = Intent(Intent.ACTION_SENDTO, "mailto:${email.trim()}".toUri())
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
if (!subject.isNullOrBlank()) {
|
if (!subject.isNullOrBlank()) {
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||||
@@ -52,12 +52,12 @@ object Intents {
|
|||||||
if (!body.isNullOrBlank()) {
|
if (!body.isNullOrBlank()) {
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, body)
|
intent.putExtra(Intent.EXTRA_TEXT, body)
|
||||||
}
|
}
|
||||||
startActivity(context, intent, null)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openWifiSettings(context: Context) {
|
fun openWifiSettings(context: Context) {
|
||||||
val intent = Intent(Settings.ACTION_WIFI_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
val intent = Intent(Settings.ACTION_WIFI_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
startActivity(context, intent, null)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addContact(context: Context, rawContent: String) {
|
fun addContact(context: Context, rawContent: String) {
|
||||||
@@ -66,7 +66,7 @@ object Intents {
|
|||||||
putExtra(ContactsContract.Intents.Insert.NOTES, rawContent)
|
putExtra(ContactsContract.Intents.Insert.NOTES, rawContent)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
startActivity(context, intent, null)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCalendarEvent(context: Context, rawContent: String) {
|
fun addCalendarEvent(context: Context, rawContent: String) {
|
||||||
@@ -76,6 +76,6 @@ object Intents {
|
|||||||
putExtra(CalendarContract.Events.DESCRIPTION, rawContent)
|
putExtra(CalendarContract.Events.DESCRIPTION, rawContent)
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
startActivity(context, intent, null)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
<string name="content_value">Inhalt</string>
|
<string name="content_value">Inhalt</string>
|
||||||
<string name="request_camera">Kamera erlauben</string>
|
<string name="request_camera">Kamera erlauben</string>
|
||||||
<string name="pinch_to_zoom_hint">Zum Zoomen bei kleinen Codes mit zwei Fingern aufziehen</string>
|
<string name="pinch_to_zoom_hint">Zum Zoomen bei kleinen Codes mit zwei Fingern aufziehen</string>
|
||||||
|
<string name="live_potential_detected">Code erkannt. Ruhig halten oder näher rangehen.</string>
|
||||||
|
<string name="live_readable_detected">Lesbarer Code erkannt.</string>
|
||||||
<string name="share_history">Historie teilen</string>
|
<string name="share_history">Historie teilen</string>
|
||||||
<string name="share_txt">TXT</string>
|
<string name="share_txt">TXT</string>
|
||||||
<string name="share_csv">CSV</string>
|
<string name="share_csv">CSV</string>
|
||||||
@@ -43,6 +45,8 @@
|
|||||||
<string name="clear_batch">Stapel leeren</string>
|
<string name="clear_batch">Stapel leeren</string>
|
||||||
<string name="share_batch">Stapel teilen</string>
|
<string name="share_batch">Stapel teilen</string>
|
||||||
<string name="no_code_found_in_image">Im gewählten Bild wurde kein QR- oder Barcode gefunden.</string>
|
<string name="no_code_found_in_image">Im gewählten Bild wurde kein QR- oder Barcode gefunden.</string>
|
||||||
|
<string name="image_scan_pick_title">%1$d Codes im Bild gefunden</string>
|
||||||
|
<string name="image_scan_pick_subtitle">Wähle ein Ergebnis aus:</string>
|
||||||
<string name="image_scan_failed">Dieses Bild konnte nicht gelesen werden. Bitte anderes Bild versuchen.</string>
|
<string name="image_scan_failed">Dieses Bild konnte nicht gelesen werden. Bitte anderes Bild versuchen.</string>
|
||||||
<string name="already_scanned">Bereits gescannt</string>
|
<string name="already_scanned">Bereits gescannt</string>
|
||||||
<string name="view_history">Historie anzeigen</string>
|
<string name="view_history">Historie anzeigen</string>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
<string name="content_value">Content</string>
|
<string name="content_value">Content</string>
|
||||||
<string name="request_camera">Allow camera</string>
|
<string name="request_camera">Allow camera</string>
|
||||||
<string name="pinch_to_zoom_hint">Pinch to zoom for small codes</string>
|
<string name="pinch_to_zoom_hint">Pinch to zoom for small codes</string>
|
||||||
|
<string name="live_potential_detected">Code spotted. Hold steady or move closer.</string>
|
||||||
|
<string name="live_readable_detected">Readable code detected.</string>
|
||||||
<string name="share_history">Share history</string>
|
<string name="share_history">Share history</string>
|
||||||
<string name="share_txt">TXT</string>
|
<string name="share_txt">TXT</string>
|
||||||
<string name="share_csv">CSV</string>
|
<string name="share_csv">CSV</string>
|
||||||
@@ -43,6 +45,8 @@
|
|||||||
<string name="clear_batch">Clear batch</string>
|
<string name="clear_batch">Clear batch</string>
|
||||||
<string name="share_batch">Share batch</string>
|
<string name="share_batch">Share batch</string>
|
||||||
<string name="no_code_found_in_image">No QR or barcode found in the selected image.</string>
|
<string name="no_code_found_in_image">No QR or barcode found in the selected image.</string>
|
||||||
|
<string name="image_scan_pick_title">Found %1$d codes in image</string>
|
||||||
|
<string name="image_scan_pick_subtitle">Choose a result to use:</string>
|
||||||
<string name="image_scan_failed">Could not read this image. Try another one.</string>
|
<string name="image_scan_failed">Could not read this image. Try another one.</string>
|
||||||
<string name="already_scanned">Already scanned</string>
|
<string name="already_scanned">Already scanned</string>
|
||||||
<string name="view_history">View history</string>
|
<string name="view_history">View history</string>
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "9.0.0" apply false
|
id("com.android.application") version "9.0.0" apply false
|
||||||
id("com.google.devtools.ksp") version "2.3.5" apply false
|
id("com.google.devtools.ksp") version "2.3.5" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.compose") version "2.2.10" apply false
|
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user