diff --git a/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt b/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt
index 813ac5d..b6e4576 100644
--- a/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt
+++ b/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt
@@ -48,6 +48,12 @@ fun UseCaseView.capabilities(): UseCaseCapabilities {
allowCopy = true,
allowShare = true,
allowOpenUrl = true,
+ allowAddContact = true,
+ allowDialPhone = true,
+ allowSendSms = true,
+ allowSendEmail = true,
+ allowOpenWifiSettings = true,
+ allowAddCalendarEvent = true,
allowHistoryExport = true
)
diff --git a/app/src/main/java/com/clean/scanner/ui/screens/ScannerGalleryPreviewDialog.kt b/app/src/main/java/com/clean/scanner/ui/screens/ScannerGalleryPreviewDialog.kt
index 0073425..3542cee 100644
--- a/app/src/main/java/com/clean/scanner/ui/screens/ScannerGalleryPreviewDialog.kt
+++ b/app/src/main/java/com/clean/scanner/ui/screens/ScannerGalleryPreviewDialog.kt
@@ -9,16 +9,23 @@ import android.os.Build
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTransformGestures
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.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -46,6 +53,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntSize
@@ -54,6 +62,7 @@ import de.softwareapp_hb.privateqrscanner.R
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionBox
import de.softwareapp_hb.privateqrscanner.data.scanner.DetectionPoint
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
+import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
import de.softwareapp_hb.privateqrscanner.util.readableBarcodePayload
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.BarcodeScanner
@@ -104,6 +113,7 @@ internal fun GalleryScanPreviewDialog(
val context = LocalContext.current
val bitmap = remember(imageUri) { imageUri?.let { loadBitmapFromUri(context, it) } }
var liveCandidates by remember(imageUri, candidates) { mutableStateOf(candidates) }
+ var selectedIndex by remember(imageUri) { mutableIntStateOf(0) }
var zoom by remember(imageUri) { mutableFloatStateOf(1f) }
var pan by remember(imageUri) { mutableStateOf(Offset.Zero) }
var viewportSize by remember { mutableStateOf(IntSize.Zero) }
@@ -130,6 +140,14 @@ internal fun GalleryScanPreviewDialog(
onDispose { scanner.close() }
}
+ LaunchedEffect(liveCandidates.size) {
+ selectedIndex = if (liveCandidates.isEmpty()) {
+ 0
+ } else {
+ selectedIndex.coerceIn(0, liveCandidates.lastIndex)
+ }
+ }
+
LaunchedEffect(bitmap, viewportSize, zoom, pan, scanTick) {
val bmp = bitmap ?: return@LaunchedEffect
val vw = viewportSize.width.toFloat()
@@ -204,14 +222,33 @@ internal fun GalleryScanPreviewDialog(
AlertDialog(
onDismissRequest = onDismiss,
- title = { Text(stringResource(R.string.image_scan_pick_title, liveCandidates.size)) },
+ containerColor = PrivateQrColors.Surface,
+ shape = RoundedCornerShape(30.dp),
+ title = {
+ Text(
+ text = stringResource(R.string.image_scan_pick_title, liveCandidates.size),
+ color = PrivateQrColors.TextPrimary,
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.ExtraBold
+ )
+ },
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ verticalArrangement = Arrangement.spacedBy(12.dp)
) {
+ Text(
+ text = if (liveCandidates.isEmpty()) {
+ stringResource(R.string.no_code_found_in_image)
+ } else {
+ stringResource(R.string.image_scan_pick_subtitle)
+ },
+ color = PrivateQrColors.TextSecondary,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.SemiBold
+ )
if (bitmap != null) {
Box(
modifier = Modifier
@@ -228,7 +265,7 @@ internal fun GalleryScanPreviewDialog(
scanTick++
}
}
- .background(Color.Black.copy(alpha = 0.32f), RoundedCornerShape(12.dp)),
+ .background(PrivateQrColors.Navy, RoundedCornerShape(24.dp)),
contentAlignment = Alignment.Center
) {
Image(
@@ -267,7 +304,7 @@ internal fun GalleryScanPreviewDialog(
liveCandidates.forEachIndexed { index, candidate ->
val box = candidate.box ?: return@forEachIndexed
- val color = Color(0xFF4AE3A3).copy(alpha = 0.96f)
+ val color = PrivateQrColors.Teal300.copy(alpha = 0.96f)
val points = box.corners.map { p ->
imageToScreen(p.x * imageW, p.y * imageH)
}
@@ -322,20 +359,45 @@ internal fun GalleryScanPreviewDialog(
}
}
- if (liveCandidates.isEmpty()) {
- Text(text = stringResource(R.string.no_code_found_in_image))
- } else {
- Text(text = stringResource(R.string.image_scan_pick_subtitle))
+ if (liveCandidates.isNotEmpty()) {
liveCandidates.forEachIndexed { index, candidate ->
- TextButton(
- onClick = { onPick(candidate) },
- modifier = Modifier.fillMaxWidth()
+ val selected = index == selectedIndex
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ color = if (selected) Color(0xFFECFDF5) else PrivateQrColors.AppBackground,
+ shape = RoundedCornerShape(24.dp)
+ )
+ .border(
+ width = 2.dp,
+ color = if (selected) PrivateQrColors.Teal300 else Color.Transparent,
+ shape = RoundedCornerShape(24.dp)
+ )
+ .clickable { selectedIndex = index }
+ .padding(18.dp),
+ horizontalArrangement = Arrangement.spacedBy(14.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
- Column(modifier = Modifier.fillMaxWidth()) {
+ Box(
+ modifier = Modifier
+ .size(54.dp)
+ .background(PrivateQrColors.Mint, RoundedCornerShape(16.dp)),
+ contentAlignment = Alignment.Center
+ ) {
Text(
- text = "${index + 1}. ${candidate.result.displayType}",
- textAlign = TextAlign.Start,
- modifier = Modifier.fillMaxWidth()
+ text = "${index + 1}",
+ color = PrivateQrColors.Teal700,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.ExtraBold
+ )
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = candidate.result.displayType,
+ color = PrivateQrColors.TextPrimary,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.ExtraBold
)
Text(
text = if (candidate.result.isBase64Encoded) {
@@ -343,10 +405,11 @@ internal fun GalleryScanPreviewDialog(
} else {
candidate.result.content
},
+ color = PrivateQrColors.TextSecondary,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.SemiBold,
maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- textAlign = TextAlign.Start,
- modifier = Modifier.fillMaxWidth()
+ overflow = TextOverflow.Ellipsis
)
}
}
@@ -354,9 +417,29 @@ internal fun GalleryScanPreviewDialog(
}
}
},
- confirmButton = {},
+ confirmButton = {
+ val selected = liveCandidates.getOrNull(selectedIndex)
+ TextButton(
+ onClick = {
+ if (selected != null) onPick(selected)
+ },
+ enabled = selected != null,
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = PrivateQrColors.Teal700,
+ contentColor = PrivateQrColors.Surface,
+ disabledContainerColor = PrivateQrColors.TextSecondary.copy(alpha = 0.18f),
+ disabledContentColor = PrivateQrColors.TextSecondary
+ ),
+ shape = RoundedCornerShape(18.dp)
+ ) {
+ Text(stringResource(R.string.image_scan_use_selected))
+ }
+ },
dismissButton = {
- TextButton(onClick = onDismiss) {
+ TextButton(
+ onClick = onDismiss,
+ colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700)
+ ) {
Text(stringResource(R.string.cancel))
}
}
diff --git a/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt
index fcdffe8..7a54c39 100644
--- a/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt
+++ b/app/src/main/java/com/clean/scanner/ui/screens/ScannerScreen.kt
@@ -801,15 +801,44 @@ fun ScannerScreen(
if (showRiskWarning && pendingOpenUrl != null) {
AlertDialog(
onDismissRequest = { showRiskWarning = false },
- text = { Text(stringResource(R.string.risk_warning)) },
+ containerColor = PrivateQrColors.Surface,
+ shape = RoundedCornerShape(28.dp),
+ title = {
+ Text(
+ text = stringResource(R.string.risk_warning_title),
+ color = PrivateQrColors.TextPrimary,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ },
+ text = {
+ Text(
+ text = stringResource(R.string.risk_warning),
+ color = PrivateQrColors.TextSecondary,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ },
confirmButton = {
- TextButton(onClick = {
- Intents.openUrl(context, pendingOpenUrl!!)
- showRiskWarning = false
- }) { Text(stringResource(R.string.open_anyway)) }
+ TextButton(
+ onClick = {
+ Intents.openUrl(context, pendingOpenUrl!!)
+ showRiskWarning = false
+ },
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = PrivateQrColors.Teal700,
+ contentColor = PrivateQrColors.Surface
+ ),
+ shape = RoundedCornerShape(18.dp)
+ ) {
+ Text(stringResource(R.string.open_anyway))
+ }
},
dismissButton = {
- TextButton(onClick = { showRiskWarning = false }) {
+ TextButton(
+ onClick = { showRiskWarning = false },
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = PrivateQrColors.Teal700
+ )
+ ) {
Text(stringResource(R.string.cancel))
}
}
diff --git a/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt
index eb290fb..ad05d55 100644
--- a/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt
+++ b/app/src/main/java/com/clean/scanner/ui/screens/SettingsScreen.kt
@@ -2,7 +2,10 @@ package de.softwareapp_hb.privateqrscanner.ui.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+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.Spacer
@@ -80,19 +83,27 @@ fun SettingsScreen(
if (showUseCasePicker.value) {
AlertDialog(
onDismissRequest = { showUseCasePicker.value = false },
- title = { Text(stringResource(R.string.select_use_case_view)) },
+ containerColor = PrivateQrColors.Surface,
+ shape = RoundedCornerShape(30.dp),
+ title = {
+ Text(
+ text = stringResource(R.string.select_use_case_view),
+ color = PrivateQrColors.TextPrimary,
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.ExtraBold
+ )
+ },
text = {
- Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
UseCaseView.entries.forEach { candidate ->
- TextButton(
+ UseCasePickerOption(
+ candidate = candidate,
+ selected = candidate == selectedUseCaseView,
onClick = {
onUseCaseViewSelected(candidate)
showUseCasePicker.value = false
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text(text = stringResource(candidate.titleRes))
- }
+ }
+ )
}
}
},
@@ -223,6 +234,61 @@ fun SettingsScreen(
}
}
+@Composable
+private fun UseCasePickerOption(
+ candidate: UseCaseView,
+ selected: Boolean,
+ onClick: () -> Unit
+) {
+ val borderColor = if (selected) PrivateQrColors.Teal300 else Color.Transparent
+ val backgroundColor = if (selected) Color(0xFFECFDF5) else PrivateQrColors.AppBackground
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(backgroundColor, RoundedCornerShape(24.dp))
+ .border(2.dp, borderColor, RoundedCornerShape(24.dp))
+ .clickable(onClick = onClick)
+ .padding(18.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(58.dp)
+ .background(PrivateQrColors.Mint, RoundedCornerShape(16.dp)),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = if (selected) "✓" else "◎",
+ color = PrivateQrColors.Teal700,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.ExtraBold
+ )
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = stringResource(candidate.titleRes),
+ color = PrivateQrColors.TextPrimary,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.ExtraBold
+ )
+ Text(
+ text = stringResource(candidate.descriptionRes()),
+ color = PrivateQrColors.TextSecondary,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ }
+}
+
+private fun UseCaseView.descriptionRes(): Int {
+ return when (this) {
+ UseCaseView.EverydayPersonal -> R.string.use_case_everyday_description
+ UseCaseView.EventTicketing -> R.string.use_case_event_ticketing_description
+ }
+}
+
@Composable
private fun SettingsHeader() {
Row(
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 0ad7721..6f0bf17 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -16,7 +16,8 @@
Öffnen
Abbrechen
Trotzdem öffnen
- Diese URL wirkt ungewöhnlich. Prüfe sie, bevor du öffnest.
+ Diese URL wirkt ungewöhnlich
+ Prüfe den Link vor dem Öffnen. Die Warnung wird auf deinem Gerät berechnet, ohne den Scan irgendwohin zu senden.
Alles löschen
Alle Historie-Einträge löschen?
Bestätigen
@@ -56,6 +57,7 @@
Im gewählten Bild wurde kein QR- oder Barcode gefunden.
%1$d Codes im Bild gefunden
Wähle ein Ergebnis aus:
+ Ausgewähltes verwenden
Dieses Bild konnte nicht gelesen werden. Bitte anderes Bild versuchen.
Bereits gescannt
Doppeltes Ticket erkannt
@@ -76,4 +78,6 @@
Use-Case-Ansicht wählen
Alltägliche private Nutzung
Events & Ticketing
+ Vollständiger privater Scanner mit lokalem Verlauf und üblichen Ergebnisaktionen.
+ Batch-Scanning, Duplikaterkennung, Whitelist-Import und Batch-Teilen.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e0565a5..b401e75 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -16,7 +16,8 @@
Open
Cancel
Open anyway
- This URL looks unusual. Check it before opening.
+ This URL looks unusual
+ Check the link before opening. The warning is calculated on your device, without sending the scan anywhere.
Delete all
Delete all history entries?
Confirm
@@ -56,6 +57,7 @@
No QR or barcode found in the selected image.
Found %1$d codes in image
Choose a result to use:
+ Use selected
Could not read this image. Try another one.
Already scanned
Duplicate ticket detected
@@ -76,4 +78,6 @@
Select use-case view
Everyday personal use
Event & ticketing
+ Full personal scanner with local history and common result actions.
+ Batch scanning, duplicate detection, whitelist import, and batch sharing.