diff --git a/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt b/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt index 33a38f0..0835fe0 100644 --- a/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt +++ b/app/src/main/java/com/clean/scanner/ui/CleanScannerAppRoot.kt @@ -2,8 +2,14 @@ package de.softwareapp_hb.privateqrscanner.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.QrCodeScanner +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,6 +26,7 @@ import de.softwareapp_hb.privateqrscanner.R import de.softwareapp_hb.privateqrscanner.ui.screens.HistoryScreen import de.softwareapp_hb.privateqrscanner.ui.screens.ScannerScreen import de.softwareapp_hb.privateqrscanner.ui.screens.SettingsScreen +import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors private enum class RootTab { Scanner, History, Settings } @@ -33,8 +40,16 @@ fun CleanScannerAppRoot(container: AppContainer) { var activeTab by remember { mutableStateOf(RootTab.Scanner) } Scaffold( + containerColor = PrivateQrColors.AppBackground, bottomBar = { - NavigationBar { + val navColors = NavigationBarItemDefaults.colors( + selectedIconColor = PrivateQrColors.Teal700, + selectedTextColor = PrivateQrColors.Teal700, + indicatorColor = PrivateQrColors.Mint, + unselectedIconColor = PrivateQrColors.TextSecondary, + unselectedTextColor = PrivateQrColors.TextSecondary + ) + NavigationBar(containerColor = PrivateQrColors.Surface) { NavigationBarItem( selected = activeTab == RootTab.Scanner, onClick = { @@ -42,7 +57,8 @@ fun CleanScannerAppRoot(container: AppContainer) { scannerViewModel.resumeScanning() }, label = { Text(stringResource(R.string.scan)) }, - icon = {} + icon = { Icon(Icons.Default.QrCodeScanner, contentDescription = null) }, + colors = navColors ) NavigationBarItem( selected = activeTab == RootTab.History, @@ -50,7 +66,8 @@ fun CleanScannerAppRoot(container: AppContainer) { activeTab = RootTab.History }, label = { Text(stringResource(R.string.history)) }, - icon = {} + icon = { Icon(Icons.Default.History, contentDescription = null) }, + colors = navColors ) NavigationBarItem( selected = activeTab == RootTab.Settings, @@ -58,7 +75,8 @@ fun CleanScannerAppRoot(container: AppContainer) { activeTab = RootTab.Settings }, label = { Text(stringResource(R.string.settings)) }, - icon = {} + icon = { Icon(Icons.Default.Settings, contentDescription = null) }, + colors = navColors ) } } 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 8263dbc..813ac5d 100644 --- a/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt +++ b/app/src/main/java/com/clean/scanner/ui/UseCaseView.kt @@ -47,7 +47,8 @@ fun UseCaseView.capabilities(): UseCaseCapabilities { allowBatchMode = false, allowCopy = true, allowShare = true, - allowOpenUrl = true + allowOpenUrl = true, + allowHistoryExport = true ) UseCaseView.EventTicketing -> UseCaseCapabilities( diff --git a/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt b/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt index 933d722..c2f5ba0 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/HistoryScreen.kt @@ -1,17 +1,36 @@ package de.softwareapp_hb.privateqrscanner.ui.screens +import androidx.compose.foundation.Image +import androidx.compose.foundation.background 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 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.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text @@ -20,14 +39,22 @@ import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import de.softwareapp_hb.privateqrscanner.R import de.softwareapp_hb.privateqrscanner.domain.ScanRecord import de.softwareapp_hb.privateqrscanner.ui.UseCaseView import de.softwareapp_hb.privateqrscanner.ui.capabilities +import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors import de.softwareapp_hb.privateqrscanner.util.HistoryExportFormatter import de.softwareapp_hb.privateqrscanner.util.Intents import java.text.DateFormat @@ -90,55 +117,97 @@ fun HistoryScreen( ) } - Column( + LazyColumn( modifier = Modifier .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.Top + .background(PrivateQrColors.AppBackground) + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - OutlinedTextField( - value = query, - onValueChange = onQueryChange, - modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(R.string.search)) } - ) - - Row(modifier = Modifier.fillMaxWidth()) { - if (capabilities.allowHistoryExport) { - TextButton( - onClick = { - val exportText = HistoryExportFormatter.formatText(history) - Intents.shareContent(context, exportText, "text/plain") - }, - enabled = history.isNotEmpty() - ) { - Text(stringResource(R.string.share_txt)) + item { + Spacer(modifier = Modifier.height(18.dp)) + HistoryHeader() + Spacer(modifier = Modifier.height(18.dp)) + Text( + text = stringResource(R.string.history), + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.ExtraBold + ) + Spacer(modifier = Modifier.height(12.dp)) + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + tint = PrivateQrColors.Teal700 + ) + }, + placeholder = { Text(stringResource(R.string.search)) }, + singleLine = true, + shape = RoundedCornerShape(20.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = PrivateQrColors.Teal300, + unfocusedBorderColor = PrivateQrColors.Divider, + focusedContainerColor = PrivateQrColors.Surface, + unfocusedContainerColor = PrivateQrColors.Surface + ) + ) + Spacer(modifier = Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (capabilities.allowHistoryExport) { + ExportButton( + text = stringResource(R.string.share_txt), + enabled = history.isNotEmpty(), + onClick = { + val exportText = HistoryExportFormatter.formatText(history) + Intents.shareContent(context, exportText, "text/plain") + } + ) + ExportButton( + text = stringResource(R.string.share_csv), + enabled = history.isNotEmpty(), + onClick = { + val exportCsv = HistoryExportFormatter.formatCsv(history) + Intents.shareContent(context, exportCsv, "text/csv") + } + ) + ExportButton( + text = stringResource(R.string.share_json), + enabled = history.isNotEmpty(), + onClick = { + val exportJson = HistoryExportFormatter.formatJson(history) + Intents.shareContent(context, exportJson, "application/json") + } + ) } + Spacer(modifier = Modifier.weight(1f)) TextButton( - onClick = { - val exportCsv = HistoryExportFormatter.formatCsv(history) - Intents.shareContent(context, exportCsv, "text/csv") - }, - enabled = history.isNotEmpty() + onClick = { showDeleteAll.value = true }, + enabled = history.isNotEmpty(), + colors = ButtonDefaults.textButtonColors( + contentColor = Color(0xFFBE123C), + disabledContentColor = PrivateQrColors.TextSecondary.copy(alpha = 0.45f) + ) ) { - Text(stringResource(R.string.share_csv)) - } - TextButton( - onClick = { - val exportJson = HistoryExportFormatter.formatJson(history) - Intents.shareContent(context, exportJson, "application/json") - }, - enabled = history.isNotEmpty() - ) { - Text(stringResource(R.string.share_json)) + Text(stringResource(R.string.delete_all), fontWeight = FontWeight.Bold) } } - TextButton(onClick = { showDeleteAll.value = true }) { - Text(stringResource(R.string.delete_all)) - } + Spacer(modifier = Modifier.height(4.dp)) } - LazyColumn { + if (history.isEmpty()) { + item { + EmptyHistoryCard() + } + } else { items(history, key = { it.id }) { item -> HistoryRow( item = item, @@ -147,6 +216,139 @@ fun HistoryScreen( ) } } + + item { + Spacer(modifier = Modifier.height(24.dp)) + } + } +} + +@Composable +private fun HistoryHeader() { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_legacy), + contentDescription = null, + modifier = Modifier.size(64.dp) + ) + Column { + Text( + text = stringResource(R.string.app_name), + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = "Optional history stays on your device", + color = PrivateQrColors.TextSecondary, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + } + } + Spacer(modifier = Modifier.height(18.dp)) + Card( + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + shape = RoundedCornerShape(30.dp) + ) { + Column( + modifier = Modifier + .background( + Brush.linearGradient( + colors = listOf(PrivateQrColors.Navy, PrivateQrColors.Teal900) + ) + ) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Review past scans locally", + color = PrivateQrColors.Surface, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = "Search, export, or delete saved scans whenever you choose.", + color = PrivateQrColors.Mint, + style = MaterialTheme.typography.titleMedium + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + PrivacyPill("No ads") + PrivacyPill("No tracking") + PrivacyPill("No account") + } + } + } +} + +@Composable +private fun PrivacyPill(text: String) { + Box( + modifier = Modifier + .background( + color = PrivateQrColors.Mint.copy(alpha = 0.12f), + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 14.dp, vertical = 9.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = PrivateQrColors.Mint, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.ExtraBold + ) + } +} + +@Composable +private fun ExportButton( + text: String, + enabled: Boolean, + onClick: () -> Unit +) { + TextButton( + onClick = onClick, + enabled = enabled, + colors = ButtonDefaults.textButtonColors( + containerColor = PrivateQrColors.Mint, + contentColor = PrivateQrColors.Teal700, + disabledContainerColor = PrivateQrColors.Mint.copy(alpha = 0.45f), + disabledContentColor = PrivateQrColors.Teal700.copy(alpha = 0.45f) + ), + shape = RoundedCornerShape(15.dp) + ) { + Text(text = text, fontWeight = FontWeight.ExtraBold) + } +} + +@Composable +private fun EmptyHistoryCard() { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface), + shape = RoundedCornerShape(24.dp) + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = "No saved scans yet", + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = "Enable local history in settings to keep a private record on this device.", + color = PrivateQrColors.TextSecondary, + style = MaterialTheme.typography.bodyMedium + ) + } } } @@ -172,25 +374,81 @@ private fun HistoryRow( state = dismissState, backgroundContent = {}, content = { - Column(modifier = Modifier - .fillMaxWidth() - .clickable { onOpenDetails() } - .padding(vertical = 12.dp)) { - Text(text = item.type) - Text( - text = if (item.isBase64Encoded()) { - stringResource(R.string.base64_encoded_inline, item.content) - } else { - item.content - }, - maxLines = 2 - ) - Text(text = DateFormat.getDateTimeInstance().format(Date(item.timestamp))) + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { onOpenDetails() }, + colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface), + shape = RoundedCornerShape(24.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + modifier = Modifier.padding(18.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(52.dp) + .background( + color = PrivateQrColors.Mint.copy(alpha = 0.55f), + shape = RoundedCornerShape(16.dp) + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = iconForType(item.type), + contentDescription = null, + tint = PrivateQrColors.Teal700 + ) + } + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = item.type, + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + .format(Date(item.timestamp)), + color = PrivateQrColors.TextSecondary, + style = MaterialTheme.typography.labelMedium, + maxLines = 1 + ) + } + Text( + text = if (item.isBase64Encoded()) { + stringResource(R.string.base64_encoded_inline, item.content) + } else { + item.content + }, + color = PrivateQrColors.TextSecondary, + style = MaterialTheme.typography.bodyLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } } } ) } +private fun iconForType(type: String): ImageVector { + return when { + type.equals("URL", ignoreCase = true) -> Icons.Default.Link + type.equals("Email", ignoreCase = true) -> Icons.Default.Email + type.equals("WiFi", ignoreCase = true) -> Icons.Default.Wifi + else -> Icons.Default.History + } +} + private fun ScanRecord.isBase64Encoded(): Boolean { return type.contains("base64", ignoreCase = true) } diff --git a/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt b/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt index d923532..c1a1ca9 100644 --- a/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt +++ b/app/src/main/java/com/clean/scanner/ui/screens/ScannerResultCards.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import de.softwareapp_hb.privateqrscanner.R import de.softwareapp_hb.privateqrscanner.domain.ScanResult +import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors import de.softwareapp_hb.privateqrscanner.util.ParsedContact import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers import de.softwareapp_hb.privateqrscanner.util.UrlRiskScorer @@ -60,39 +61,59 @@ internal fun ResultVisualCard( val fields = remember(result) { buildResultFields(result) } Card( modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = Color(0xFFF2F7FF)), - shape = RoundedCornerShape(14.dp) + colors = CardDefaults.cardColors(containerColor = PrivateQrColors.SoftSurface), + shape = RoundedCornerShape(26.dp) ) { Column( - modifier = Modifier.padding(14.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.padding(22.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) ) { if (!result.isBase64Encoded && result.type == "WiFi") { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = Icons.Default.Wifi, contentDescription = null, - tint = Color(0xFF1D4ED8) + tint = PrivateQrColors.Teal700 ) Text( text = "Wi-Fi", - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.headlineSmall ) } } else { - Text( - text = result.displayType, - style = MaterialTheme.typography.titleMedium - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = result.displayType, + style = MaterialTheme.typography.headlineSmall, + color = PrivateQrColors.TextPrimary + ) + if (!result.isBase64Encoded && result.type == "URL") { + Text( + text = "Local check", + color = PrivateQrColors.Teal700, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .background( + color = PrivateQrColors.Mint, + shape = RoundedCornerShape(50) + ) + .padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + } } if (result.isBase64Encoded) { Text( text = stringResource(R.string.base64_encoded_notice), style = MaterialTheme.typography.bodySmall, - color = Color(0xFF4F6277) + color = PrivateQrColors.TextSecondary ) } if (fields.isEmpty()) { @@ -106,7 +127,7 @@ internal fun ResultVisualCard( Text( text = field.label, style = MaterialTheme.typography.labelMedium, - color = Color(0xFF4F6277) + color = PrivateQrColors.TextSecondary ) val isClickableUrl = result.type == "URL" && field.label == "Link" && @@ -114,7 +135,7 @@ internal fun ResultVisualCard( Text( text = field.value, style = MaterialTheme.typography.bodyMedium, - color = if (isClickableUrl) Color(0xFF1D4ED8) else Color.Unspecified, + color = if (isClickableUrl) Color(0xFF1D4ED8) else PrivateQrColors.TextPrimary, textDecoration = if (isClickableUrl) TextDecoration.Underline else null, modifier = if (isClickableUrl) { Modifier.clickable { onOpenUrl(field.value) } @@ -127,6 +148,30 @@ internal fun ResultVisualCard( } } } + if (!result.isBase64Encoded && result.type == "URL") { + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = Color(0xFFECFDF5), + shape = RoundedCornerShape(18.dp) + ) + .padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "✓", + color = PrivateQrColors.Teal700, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "Checked on device before opening", + color = PrivateQrColors.Teal700, + style = MaterialTheme.typography.labelLarge + ) + } + } } } } 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 41353f3..fcdffe8 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 @@ -34,9 +34,11 @@ import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material.icons.filled.ViewModule import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -57,6 +59,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke @@ -81,6 +84,7 @@ import de.softwareapp_hb.privateqrscanner.ui.EventTicketScanDecision import de.softwareapp_hb.privateqrscanner.ui.UseCaseView import com.clean.scanner.ui.components.CameraPreview import de.softwareapp_hb.privateqrscanner.ui.capabilities +import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors import de.softwareapp_hb.privateqrscanner.util.ClipboardUtil import de.softwareapp_hb.privateqrscanner.util.Intents import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers @@ -317,14 +321,15 @@ fun ScannerScreen( Box( modifier = Modifier .fillMaxSize() + .background(PrivateQrColors.Deep) .onSizeChanged { containerSize = it } ) { val density = LocalDensity.current val viewW = containerSize.width.toFloat() val viewH = containerSize.height.toFloat() val galleryOpen = imageScanPreviewUri != null - val aimW = viewW * 0.62f - val aimH = with(density) { 200.dp.toPx() } + val aimW = viewW * 0.70f + val aimH = with(density) { 230.dp.toPx() } val aimLeft = (viewW - aimW) / 2f val aimTop = (viewH - aimH) / 2f val aimRight = aimLeft + aimW @@ -385,6 +390,20 @@ fun ScannerScreen( } ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + colors = listOf( + PrivateQrColors.Deep.copy(alpha = 0.72f), + PrivateQrColors.Teal900.copy(alpha = 0.34f), + PrivateQrColors.Deep.copy(alpha = 0.78f) + ) + ) + ) + ) + if (detectionBoxes.isNotEmpty()) { Canvas( modifier = Modifier @@ -421,15 +440,15 @@ fun ScannerScreen( drawPath( path = outline, color = boxColor.copy(alpha = 0.96f), - style = Stroke(width = 4f) + style = Stroke(width = 5.5f) ) } else if (right > left && bottom > top) { drawRoundRect( color = boxColor.copy(alpha = 0.95f), topLeft = Offset(left, top), size = Size(right - left, bottom - top), - cornerRadius = CornerRadius(14f, 14f), - style = Stroke(width = 4f) + cornerRadius = CornerRadius(22f, 22f), + style = Stroke(width = 5.5f) ) } } @@ -439,22 +458,22 @@ fun ScannerScreen( Canvas( modifier = Modifier .align(Alignment.Center) - .fillMaxWidth(0.62f) - .height(200.dp) + .fillMaxWidth(0.70f) + .height(230.dp) ) { val guideColor = when { hasReadableInView -> Color(0xFF4AE3A3) - hasPotentialInView -> Color(0xFFFFC857) - else -> Color(0xFF7CE6C6) + hasPotentialInView -> PrivateQrColors.Warning + else -> PrivateQrColors.Teal300 } drawRoundRect( - color = guideColor.copy(alpha = 0.08f), - cornerRadius = CornerRadius(22f, 22f) + color = guideColor.copy(alpha = 0.12f), + cornerRadius = CornerRadius(32f, 32f) ) drawRoundRect( color = guideColor.copy(alpha = 0.90f), - cornerRadius = CornerRadius(22f, 22f), - style = Stroke(width = 3.5f) + cornerRadius = CornerRadius(32f, 32f), + style = Stroke(width = 5f) ) } @@ -465,15 +484,16 @@ fun ScannerScreen( else -> stringResource(R.string.aim_center_hint) }, color = Color.White, + style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center, modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = if (isBatchModeActive) 190.dp else 56.dp) .background( - color = Color.Black.copy(alpha = 0.35f), - shape = RoundedCornerShape(18.dp) + color = Color.Black.copy(alpha = 0.45f), + shape = RoundedCornerShape(24.dp) ) - .padding(horizontal = 14.dp, vertical = 8.dp) + .padding(horizontal = 18.dp, vertical = 10.dp) ) if (capabilities.allowScanFromImage) { @@ -483,8 +503,8 @@ fun ScannerScreen( .align(Alignment.TopEnd) .padding(top = 12.dp, end = 12.dp) .background( - color = Color.Black.copy(alpha = 0.35f), - shape = RoundedCornerShape(14.dp) + color = Color.Black.copy(alpha = 0.38f), + shape = RoundedCornerShape(18.dp) ) ) { Icon( @@ -501,8 +521,8 @@ fun ScannerScreen( .align(Alignment.TopEnd) .padding(top = 12.dp, end = if (capabilities.allowScanFromImage) 64.dp else 12.dp) .background( - color = Color.Black.copy(alpha = 0.35f), - shape = RoundedCornerShape(14.dp) + color = Color.Black.copy(alpha = 0.38f), + shape = RoundedCornerShape(18.dp) ) ) { Icon( @@ -516,15 +536,16 @@ fun ScannerScreen( Text( text = stringResource(useCaseView.titleRes), color = Color.White, + style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, modifier = Modifier .align(Alignment.TopCenter) .padding(top = 16.dp, start = 64.dp, end = 64.dp) .background( - color = Color.Black.copy(alpha = 0.4f), - shape = RoundedCornerShape(14.dp) + color = Color.Black.copy(alpha = 0.52f), + shape = RoundedCornerShape(22.dp) ) - .padding(horizontal = 12.dp, vertical = 6.dp) + .padding(horizontal = 18.dp, vertical = 10.dp) ) if (useCaseView == UseCaseView.EventTicketing) { Text( @@ -613,12 +634,16 @@ fun ScannerScreen( if (lastResult.isBase64Encoded) null else ScanContentParsers.parseCalendarEvent(lastResult.content) } - ModalBottomSheet(onDismissRequest = onScanAgain) { + ModalBottomSheet( + onDismissRequest = onScanAgain, + containerColor = PrivateQrColors.Surface, + shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) + ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(start = 20.dp, top = 4.dp, end = 20.dp, bottom = 28.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) ) { ResultVisualCard( result = lastResult, @@ -642,31 +667,50 @@ fun ScannerScreen( modifier = Modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { if (capabilities.allowAddContact && parsedContact != null) { - IconButton(onClick = { - Intents.addContact(context, parsedContact, lastResult.content) - }) { + IconButton( + onClick = { Intents.addContact(context, parsedContact, lastResult.content) }, + modifier = Modifier.background( + color = PrivateQrColors.AppBackground, + shape = RoundedCornerShape(50) + ) + ) { Icon( imageVector = Icons.Default.PersonAdd, - contentDescription = stringResource(R.string.add_contact) + contentDescription = stringResource(R.string.add_contact), + tint = PrivateQrColors.TextPrimary ) } } if (capabilities.allowCopy) { - IconButton(onClick = { ClipboardUtil.copy(context, lastResult.content) }) { + IconButton( + onClick = { ClipboardUtil.copy(context, lastResult.content) }, + modifier = Modifier.background( + color = PrivateQrColors.AppBackground, + shape = RoundedCornerShape(50) + ) + ) { Icon( imageVector = Icons.Default.ContentCopy, - contentDescription = stringResource(R.string.copy) + contentDescription = stringResource(R.string.copy), + tint = PrivateQrColors.TextPrimary ) } } if (capabilities.allowShare) { - IconButton(onClick = { Intents.shareText(context, lastResult.content) }) { + IconButton( + onClick = { Intents.shareText(context, lastResult.content) }, + modifier = Modifier.background( + color = PrivateQrColors.AppBackground, + shape = RoundedCornerShape(50) + ) + ) { Icon( imageVector = Icons.Default.Share, - contentDescription = stringResource(R.string.share) + contentDescription = stringResource(R.string.share), + tint = PrivateQrColors.TextPrimary ) } } @@ -677,9 +721,14 @@ fun ScannerScreen( when (lastResult.type) { "Phone" -> { if (capabilities.allowDialPhone) { - Button(onClick = { - Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content)) - }) { + Button( + onClick = { + Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content)) + }, + colors = ButtonDefaults.buttonColors( + containerColor = PrivateQrColors.Teal700 + ) + ) { Text(stringResource(R.string.call_number)) } } @@ -687,10 +736,15 @@ fun ScannerScreen( "SMS" -> { if (capabilities.allowSendSms) { - Button(onClick = { - val smsData = ScanContentParsers.parseSms(lastResult.content) - Intents.sendSms(context, smsData.first, smsData.second) - }) { + Button( + onClick = { + val smsData = ScanContentParsers.parseSms(lastResult.content) + Intents.sendSms(context, smsData.first, smsData.second) + }, + colors = ButtonDefaults.buttonColors( + containerColor = PrivateQrColors.Teal700 + ) + ) { Text(stringResource(R.string.send_sms)) } } @@ -698,9 +752,14 @@ fun ScannerScreen( "Email" -> { if (capabilities.allowSendEmail) { - Button(onClick = { - Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null) - }) { + Button( + onClick = { + Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null) + }, + colors = ButtonDefaults.buttonColors( + containerColor = PrivateQrColors.Teal700 + ) + ) { Text(stringResource(R.string.send_email)) } } @@ -708,7 +767,12 @@ fun ScannerScreen( "WiFi" -> { if (capabilities.allowOpenWifiSettings) { - Button(onClick = { Intents.openWifiSettings(context) }) { + Button( + onClick = { Intents.openWifiSettings(context) }, + colors = ButtonDefaults.buttonColors( + containerColor = PrivateQrColors.Teal700 + ) + ) { Text(stringResource(R.string.open_wifi_settings)) } } @@ -716,9 +780,14 @@ fun ScannerScreen( "Calendar" -> { if (capabilities.allowAddCalendarEvent) { - Button(onClick = { - Intents.addCalendarEvent(context, parsedEvent, lastResult.content) - }) { + Button( + onClick = { + Intents.addCalendarEvent(context, parsedEvent, lastResult.content) + }, + colors = ButtonDefaults.buttonColors( + containerColor = PrivateQrColors.Teal700 + ) + ) { Text(stringResource(R.string.add_calendar_event)) } } 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 fa1f1f5..eb290fb 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 @@ -1,25 +1,44 @@ package de.softwareapp_hb.privateqrscanner.ui.screens +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import de.softwareapp_hb.privateqrscanner.R import de.softwareapp_hb.privateqrscanner.ui.UseCaseView +import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors import de.softwareapp_hb.privateqrscanner.util.InAppReviewRequester @Composable @@ -93,50 +112,216 @@ fun SettingsScreen( Column( modifier = Modifier .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.Top + .background(PrivateQrColors.AppBackground) + .verticalScroll(rememberScrollState()) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(18.dp) ) { - Text(text = stringResource(R.string.save_history)) - Switch( - checked = historyEnabled, - onCheckedChange = { enabled -> - if (!enabled && historyEnabled) { - showDeleteConfirm.value = true - } else { - onHistoryToggle(enabled, false) - } - } + SettingsHeader() + + Text( + text = stringResource(R.string.settings), + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.ExtraBold ) - Spacer(modifier = Modifier.height(16.dp)) - - Text(text = stringResource(R.string.security_warnings)) - Switch(checked = warningsEnabled, onCheckedChange = onWarningsToggle) - - Spacer(modifier = Modifier.height(16.dp)) - - Text(text = stringResource(R.string.scan_feedback)) - Switch(checked = scanFeedbackEnabled, onCheckedChange = onScanFeedbackToggle) - - Spacer(modifier = Modifier.height(16.dp)) - - Text(text = stringResource(R.string.active_use_case_view)) - Text(text = stringResource(selectedUseCaseView.titleRes)) - TextButton(onClick = { showUseCasePicker.value = true }) { - Text(stringResource(R.string.select_use_case_view)) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface), + shape = RoundedCornerShape(28.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)) { + SettingsToggleRow( + title = stringResource(R.string.save_history), + checked = historyEnabled, + onCheckedChange = { enabled -> + if (!enabled && historyEnabled) { + showDeleteConfirm.value = true + } else { + onHistoryToggle(enabled, false) + } + } + ) + SettingsDivider() + SettingsToggleRow( + title = stringResource(R.string.security_warnings), + checked = warningsEnabled, + onCheckedChange = onWarningsToggle + ) + SettingsDivider() + SettingsToggleRow( + title = stringResource(R.string.scan_feedback), + checked = scanFeedbackEnabled, + onCheckedChange = onScanFeedbackToggle + ) + } } - Spacer(modifier = Modifier.height(24.dp)) - Text(text = stringResource(R.string.about)) - Text(text = stringResource(R.string.version)) - Text(text = stringResource(R.string.licenses)) - Text(text = stringResource(R.string.contact)) - TextButton(onClick = { showPrivacyPolicy.value = true }) { - Text(text = stringResource(R.string.privacy_policy)) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface), + shape = RoundedCornerShape(28.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = stringResource(R.string.active_use_case_view), + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = stringResource(selectedUseCaseView.titleRes), + color = PrivateQrColors.TextSecondary, + style = MaterialTheme.typography.bodyLarge + ) + TextButton( + onClick = { showUseCasePicker.value = true }, + colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700) + ) { + Text(stringResource(R.string.select_use_case_view), fontWeight = FontWeight.Bold) + } + } } + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface), + shape = RoundedCornerShape(28.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = stringResource(R.string.about), + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.ExtraBold + ) + InfoLine(stringResource(R.string.version)) + InfoLine(stringResource(R.string.licenses)) + InfoLine(stringResource(R.string.contact)) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + TextButton( + onClick = { showPrivacyPolicy.value = true }, + colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700) + ) { + Text(text = stringResource(R.string.privacy_policy), fontWeight = FontWeight.Bold) + } + TextButton( + onClick = { InAppReviewRequester.requestReview(context) }, + colors = ButtonDefaults.textButtonColors(contentColor = PrivateQrColors.Teal700) + ) { + Text(text = stringResource(R.string.review_app), fontWeight = FontWeight.Bold) + } + } + } + } + Spacer(modifier = Modifier.height(12.dp)) - TextButton(onClick = { InAppReviewRequester.requestReview(context) }) { - Text(text = stringResource(R.string.review_app)) + } +} + +@Composable +private fun SettingsHeader() { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_legacy), + contentDescription = null, + modifier = Modifier.size(64.dp) + ) + Column { + Text( + text = stringResource(R.string.app_name), + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = "Local privacy controls", + color = PrivateQrColors.TextSecondary, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + } + } + Spacer(modifier = Modifier.height(18.dp)) + Card( + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + shape = RoundedCornerShape(30.dp) + ) { + Column( + modifier = Modifier + .background( + Brush.linearGradient( + colors = listOf(PrivateQrColors.Navy, PrivateQrColors.Teal900) + ) + ) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Control what stays saved", + color = PrivateQrColors.Surface, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = "History, warnings, and feedback are optional device-local settings.", + color = PrivateQrColors.Mint, + style = MaterialTheme.typography.titleMedium + ) } } } + +@Composable +private fun SettingsToggleRow( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(74.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = PrivateQrColors.TextPrimary, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.ExtraBold + ) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = PrivateQrColors.Surface, + checkedTrackColor = PrivateQrColors.Teal700, + uncheckedThumbColor = PrivateQrColors.Surface, + uncheckedTrackColor = PrivateQrColors.TextSecondary.copy(alpha = 0.35f) + ) + ) + } +} + +@Composable +private fun SettingsDivider() { + HorizontalDivider(color = PrivateQrColors.Divider) +} + +@Composable +private fun InfoLine(text: String) { + Text( + text = text, + color = PrivateQrColors.TextSecondary, + style = MaterialTheme.typography.bodyMedium + ) +} diff --git a/app/src/main/java/com/clean/scanner/ui/theme/PrivateQrColors.kt b/app/src/main/java/com/clean/scanner/ui/theme/PrivateQrColors.kt new file mode 100644 index 0000000..2448d80 --- /dev/null +++ b/app/src/main/java/com/clean/scanner/ui/theme/PrivateQrColors.kt @@ -0,0 +1,21 @@ +package de.softwareapp_hb.privateqrscanner.ui.theme + +import androidx.compose.ui.graphics.Color + +object PrivateQrColors { + val Deep = Color(0xFF07111F) + val Navy = Color(0xFF0B1220) + val Teal900 = Color(0xFF123B3F) + val Teal800 = Color(0xFF155E63) + val Teal700 = Color(0xFF0F766E) + val Teal300 = Color(0xFF2DD4BF) + val Mint = Color(0xFFDFF7F2) + val AppBackground = Color(0xFFF6FBFA) + val Surface = Color(0xFFFFFFFF) + val SoftSurface = Color(0xFFF2F7FF) + val TextPrimary = Color(0xFF0B1220) + val TextSecondary = Color(0xFF607080) + val Divider = Color(0xFFDCE8E6) + val Success = Color(0xFF10B981) + val Warning = Color(0xFFFFC857) +} diff --git a/app/src/main/java/com/clean/scanner/ui/theme/Theme.kt b/app/src/main/java/com/clean/scanner/ui/theme/Theme.kt index c1a85aa..d1d3ebf 100644 --- a/app/src/main/java/com/clean/scanner/ui/theme/Theme.kt +++ b/app/src/main/java/com/clean/scanner/ui/theme/Theme.kt @@ -1,28 +1,41 @@ package de.softwareapp_hb.privateqrscanner.ui.theme -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -private val LightColors = lightColorScheme() -private val DarkColors = darkColorScheme() +private val LightColors = lightColorScheme( + primary = PrivateQrColors.Teal700, + onPrimary = PrivateQrColors.Surface, + secondary = PrivateQrColors.Teal300, + onSecondary = PrivateQrColors.Navy, + background = PrivateQrColors.AppBackground, + onBackground = PrivateQrColors.TextPrimary, + surface = PrivateQrColors.Surface, + onSurface = PrivateQrColors.TextPrimary, + surfaceVariant = PrivateQrColors.Mint, + onSurfaceVariant = PrivateQrColors.TextSecondary +) + +private val DarkColors = darkColorScheme( + primary = PrivateQrColors.Teal300, + onPrimary = PrivateQrColors.Navy, + secondary = PrivateQrColors.Mint, + onSecondary = PrivateQrColors.Navy, + background = PrivateQrColors.Deep, + onBackground = PrivateQrColors.Surface, + surface = PrivateQrColors.Navy, + onSurface = PrivateQrColors.Surface, + surfaceVariant = PrivateQrColors.Teal900, + onSurfaceVariant = PrivateQrColors.Mint +) @Composable fun CleanScannerTheme(content: @Composable () -> Unit) { val darkTheme = isSystemInDarkTheme() - val context = LocalContext.current - - val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } else { - if (darkTheme) DarkColors else LightColors - } + val colorScheme = if (darkTheme) DarkColors else LightColors MaterialTheme( colorScheme = colorScheme, diff --git a/store-assets/private-qr-scanner-phone-screenshot-1.html b/store-assets/private-qr-scanner-phone-screenshot-1.html new file mode 100644 index 0000000..b37ff47 --- /dev/null +++ b/store-assets/private-qr-scanner-phone-screenshot-1.html @@ -0,0 +1,515 @@ + + + + + + Private QR Scanner Screenshot 1 + + + +
+
+ +
+
9:41
+
+
+
+
+
+ +
Everyday personal use
+ + +
+
+
+ + + + + +
+
+
+
+
Readable code detected.
+ +
+
+
+

URL Local check

+
+
Link
+
https://example.org/menu
+
+
+
Risk score
+
0
+
+
+ + + + + Checked on device before opening +
+
+ +
+ + +
Open
+
+
+ + +
+ + diff --git a/store-assets/private-qr-scanner-phone-screenshot-1.png b/store-assets/private-qr-scanner-phone-screenshot-1.png new file mode 100644 index 0000000..d4f054f Binary files /dev/null and b/store-assets/private-qr-scanner-phone-screenshot-1.png differ diff --git a/store-assets/private-qr-scanner-phone-screenshot-2.html b/store-assets/private-qr-scanner-phone-screenshot-2.html new file mode 100644 index 0000000..fb14afb --- /dev/null +++ b/store-assets/private-qr-scanner-phone-screenshot-2.html @@ -0,0 +1,525 @@ + + + + + + Private QR Scanner Screenshot 2 + + + +
+
+
9:41
+
+
+
+
+
+ +
+
+ +
+

Private QR Scanner

+
Optional history stays on your device
+
+
+ +
+
+
+

Review past scans locally

+

Search, export, or delete saved scans whenever you choose.

+
+ +
+
+
No ads
+
No tracking
+
No account
+
+
+ +

History

+ +
+ + + + +
+ +
+
+
+ + + + +
+
+
URL
Today, 9:38 AM
+
https://example.org/menu
+
+
+ +
+
+ + + + +
+
+
Email
Today, 9:21 AM
+
support@example.org
+
+
+ +
+
+ + + + + +
+
+
Wi-Fi
Yesterday
+
SSID: Guest Network
+
+
+
+ +
+
+ Save history (local) + +
+
+ Security warnings + +
+
+ Scan feedback + +
+
+
+ + +
+ + diff --git a/store-assets/private-qr-scanner-phone-screenshot-2.png b/store-assets/private-qr-scanner-phone-screenshot-2.png new file mode 100644 index 0000000..82ec1fb Binary files /dev/null and b/store-assets/private-qr-scanner-phone-screenshot-2.png differ