Compare commits

..

4 Commits

Author SHA1 Message Date
Hadrian Burkhardt 5d5284d76e view alignment 2026-05-10 11:05:06 +02:00
Hadrian Burkhardt d822e54f91 phone screen shots 2026-05-10 10:53:37 +02:00
Hadrian Burkhardt 1eb389ea5e nicer visual aperance 2026-05-10 10:31:51 +02:00
Hadrian Burkhardt 120d1672a3 feature graphic added 2026-05-10 08:49:07 +02:00
29 changed files with 3776 additions and 207 deletions
@@ -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
)
}
}
@@ -47,7 +47,14 @@ fun UseCaseView.capabilities(): UseCaseCapabilities {
allowBatchMode = false,
allowCopy = true,
allowShare = true,
allowOpenUrl = true
allowOpenUrl = true,
allowAddContact = true,
allowDialPhone = true,
allowSendSms = true,
allowSendEmail = true,
allowOpenWifiSettings = true,
allowAddCalendarEvent = true,
allowHistoryExport = true
)
UseCaseView.EventTicketing -> UseCaseCapabilities(
@@ -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)
) {
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(),
label = { Text(stringResource(R.string.search)) }
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
tint = PrivateQrColors.Teal700
)
Row(modifier = Modifier.fillMaxWidth()) {
},
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) {
TextButton(
ExportButton(
text = stringResource(R.string.share_txt),
enabled = history.isNotEmpty(),
onClick = {
val exportText = HistoryExportFormatter.formatText(history)
Intents.shareContent(context, exportText, "text/plain")
},
enabled = history.isNotEmpty()
) {
Text(stringResource(R.string.share_txt))
}
TextButton(
)
ExportButton(
text = stringResource(R.string.share_csv),
enabled = history.isNotEmpty(),
onClick = {
val exportCsv = HistoryExportFormatter.formatCsv(history)
Intents.shareContent(context, exportCsv, "text/csv")
},
enabled = history.isNotEmpty()
) {
Text(stringResource(R.string.share_csv))
}
TextButton(
)
ExportButton(
text = stringResource(R.string.share_json),
enabled = history.isNotEmpty(),
onClick = {
val exportJson = HistoryExportFormatter.formatJson(history)
Intents.shareContent(context, exportJson, "application/json")
},
enabled = history.isNotEmpty()
}
)
}
Spacer(modifier = Modifier.weight(1f))
TextButton(
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_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
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onOpenDetails() }
.padding(vertical = 12.dp)) {
Text(text = item.type)
.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
},
maxLines = 2
color = PrivateQrColors.TextSecondary,
style = MaterialTheme.typography.bodyLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(text = DateFormat.getDateTimeInstance().format(Date(item.timestamp)))
}
}
}
}
)
}
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)
}
@@ -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
) {
Box(
modifier = Modifier
.size(54.dp)
.background(PrivateQrColors.Mint, RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) {
Column(modifier = Modifier.fillMaxWidth()) {
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))
}
}
@@ -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 {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = result.displayType,
style = MaterialTheme.typography.titleMedium
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
)
}
}
}
}
}
@@ -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 = {
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 = {
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 = {
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 = {
Button(
onClick = {
Intents.addCalendarEvent(context, parsedEvent, lastResult.content)
}) {
},
colors = ButtonDefaults.buttonColors(
containerColor = PrivateQrColors.Teal700
)
) {
Text(stringResource(R.string.add_calendar_event))
}
}
@@ -732,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 = {
TextButton(
onClick = {
Intents.openUrl(context, pendingOpenUrl!!)
showRiskWarning = false
}) { Text(stringResource(R.string.open_anyway)) }
},
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))
}
}
@@ -1,25 +1,47 @@
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
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
@@ -61,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))
}
)
}
}
},
@@ -93,11 +123,29 @@ 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(
SettingsHeader()
Text(
text = stringResource(R.string.settings),
color = PrivateQrColors.TextPrimary,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.ExtraBold
)
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) {
@@ -107,36 +155,239 @@ fun SettingsScreen(
}
}
)
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))
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 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(
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
)
}
@@ -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)
}
@@ -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,
+5 -1
View File
@@ -16,7 +16,8 @@
<string name="open">Öffnen</string>
<string name="cancel">Abbrechen</string>
<string name="open_anyway">Trotzdem öffnen</string>
<string name="risk_warning">Diese URL wirkt ungewöhnlich. Prüfe sie, bevor du öffnest.</string>
<string name="risk_warning_title">Diese URL wirkt ungewöhnlich</string>
<string name="risk_warning">Prüfe den Link vor dem Öffnen. Die Warnung wird auf deinem Gerät berechnet, ohne den Scan irgendwohin zu senden.</string>
<string name="delete_all">Alles löschen</string>
<string name="confirm_delete_all">Alle Historie-Einträge löschen?</string>
<string name="confirm">Bestätigen</string>
@@ -56,6 +57,7 @@
<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_use_selected">Ausgewähltes verwenden</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="duplicate_ticket_alert_title">Doppeltes Ticket erkannt</string>
@@ -76,4 +78,6 @@
<string name="select_use_case_view">Use-Case-Ansicht wählen</string>
<string name="use_case_everyday_personal">Alltägliche private Nutzung</string>
<string name="use_case_event_ticketing">Events &amp; Ticketing</string>
<string name="use_case_everyday_description">Vollständiger privater Scanner mit lokalem Verlauf und üblichen Ergebnisaktionen.</string>
<string name="use_case_event_ticketing_description">Batch-Scanning, Duplikaterkennung, Whitelist-Import und Batch-Teilen.</string>
</resources>
+5 -1
View File
@@ -16,7 +16,8 @@
<string name="open">Open</string>
<string name="cancel">Cancel</string>
<string name="open_anyway">Open anyway</string>
<string name="risk_warning">This URL looks unusual. Check it before opening.</string>
<string name="risk_warning_title">This URL looks unusual</string>
<string name="risk_warning">Check the link before opening. The warning is calculated on your device, without sending the scan anywhere.</string>
<string name="delete_all">Delete all</string>
<string name="confirm_delete_all">Delete all history entries?</string>
<string name="confirm">Confirm</string>
@@ -56,6 +57,7 @@
<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_use_selected">Use selected</string>
<string name="image_scan_failed">Could not read this image. Try another one.</string>
<string name="already_scanned">Already scanned</string>
<string name="duplicate_ticket_alert_title">Duplicate ticket detected</string>
@@ -76,4 +78,6 @@
<string name="select_use_case_view">Select use-case view</string>
<string name="use_case_everyday_personal">Everyday personal use</string>
<string name="use_case_event_ticketing">Event &amp; ticketing</string>
<string name="use_case_everyday_description">Full personal scanner with local history and common result actions.</string>
<string name="use_case_event_ticketing_description">Batch scanning, duplicate detection, whitelist import, and batch sharing.</string>
</resources>
@@ -0,0 +1,389 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1024, height=500, initial-scale=1">
<title>Private QR Scanner Feature Graphic</title>
<style>
:root {
--ink: #f8fafc;
--muted: #cbe7e3;
--deep: #07111f;
--navy: #0b1220;
--teal-900: #0f3f45;
--teal-700: #155e63;
--teal-300: #2dd4bf;
--mint: #dff7f2;
}
* {
box-sizing: border-box;
}
html,
body {
width: 1024px;
height: 500px;
margin: 0;
overflow: hidden;
background: var(--deep);
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
}
body {
display: grid;
place-items: stretch;
}
.feature {
position: relative;
width: 1024px;
height: 500px;
overflow: hidden;
color: var(--ink);
background:
radial-gradient(circle at 78% 34%, rgba(45, 212, 191, 0.24), transparent 29%),
radial-gradient(circle at 12% 86%, rgba(223, 247, 242, 0.12), transparent 24%),
linear-gradient(132deg, #0b1220 0%, #103a42 50%, #07111f 100%);
}
.feature::before,
.feature::after {
content: "";
position: absolute;
left: -80px;
right: -80px;
pointer-events: none;
}
.feature::before {
top: 210px;
height: 210px;
background: #061525;
clip-path: polygon(0 58%, 16% 29%, 33% 20%, 51% 33%, 70% 36%, 88% 21%, 100% 8%, 100% 100%, 0 100%);
opacity: 0.82;
}
.feature::after {
top: -32px;
height: 214px;
background: #1f7778;
clip-path: polygon(0 0, 100% 0, 100% 46%, 83% 65%, 67% 58%, 51% 40%, 31% 35%, 15% 54%, 0 75%);
opacity: 0.36;
}
.grid {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 506px 1fr;
gap: 38px;
height: 100%;
padding: 56px 64px 48px 74px;
align-items: center;
}
.copy {
align-self: center;
}
.kicker {
display: inline-flex;
align-items: center;
gap: 10px;
height: 38px;
padding: 0 16px 0 12px;
border-radius: 999px;
color: var(--mint);
background: rgba(223, 247, 242, 0.1);
border: 1px solid rgba(223, 247, 242, 0.22);
font-size: 17px;
font-weight: 700;
letter-spacing: 0;
}
.kicker svg {
width: 20px;
height: 20px;
flex: 0 0 auto;
}
h1 {
margin: 18px 0 0;
color: var(--ink);
font-size: 68px;
line-height: 0.98;
font-weight: 850;
letter-spacing: 0;
}
.subtitle {
width: 445px;
margin: 22px 0 0;
color: var(--muted);
font-size: 28px;
line-height: 1.23;
font-weight: 520;
letter-spacing: 0;
}
.checks {
display: flex;
gap: 12px;
margin-top: 32px;
}
.check {
display: inline-flex;
align-items: center;
gap: 9px;
height: 48px;
padding: 0 17px 0 14px;
border-radius: 14px;
color: #061525;
background: var(--mint);
font-size: 22px;
font-weight: 800;
white-space: nowrap;
box-shadow: 0 10px 24px rgba(2, 6, 23, 0.22);
}
.check:nth-child(2) {
background: var(--teal-300);
}
.check:nth-child(3) {
background: #f8fafc;
}
.check svg {
width: 19px;
height: 19px;
flex: 0 0 auto;
}
.visual {
position: relative;
height: 390px;
}
.halo {
position: absolute;
inset: 16px 5px 12px 34px;
border-radius: 44px;
background: rgba(223, 247, 242, 0.06);
border: 1px solid rgba(223, 247, 242, 0.08);
transform: rotate(-5deg);
}
.phone {
position: absolute;
right: 40px;
top: 9px;
width: 238px;
height: 374px;
border-radius: 36px;
padding: 15px;
background: linear-gradient(145deg, #e5fbf7, #f8fafc);
box-shadow: 0 28px 48px rgba(2, 6, 23, 0.42);
}
.phone::before {
content: "";
position: absolute;
top: 9px;
left: 91px;
width: 56px;
height: 7px;
border-radius: 999px;
background: #0b1220;
opacity: 0.8;
}
.screen {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 25px;
background:
linear-gradient(90deg, rgba(45, 212, 191, 0.12) 1px, transparent 1px) 0 0 / 28px 28px,
linear-gradient(0deg, rgba(45, 212, 191, 0.12) 1px, transparent 1px) 0 0 / 28px 28px,
radial-gradient(circle at 47% 42%, rgba(45, 212, 191, 0.18), transparent 27%),
linear-gradient(160deg, #07111f 0%, #123b3f 100%);
}
.scan-window {
position: absolute;
left: 39px;
top: 77px;
width: 132px;
height: 132px;
}
.corner {
position: absolute;
width: 36px;
height: 36px;
border-color: var(--teal-300);
border-style: solid;
}
.corner.tl {
top: 0;
left: 0;
border-width: 7px 0 0 7px;
border-radius: 13px 0 0 0;
}
.corner.tr {
top: 0;
right: 0;
border-width: 7px 7px 0 0;
border-radius: 0 13px 0 0;
}
.corner.bl {
left: 0;
bottom: 0;
border-width: 0 0 7px 7px;
border-radius: 0 0 0 13px;
}
.corner.br {
right: 0;
bottom: 0;
border-width: 0 7px 7px 0;
border-radius: 0 0 13px 0;
}
.qr {
position: absolute;
inset: 21px;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
padding: 5px;
border-radius: 8px;
background: rgba(248, 250, 252, 0.96);
}
.qr span {
border-radius: 2px;
background: #08111f;
}
.qr span:nth-child(5n+3),
.qr span:nth-child(7),
.qr span:nth-child(18) {
background: var(--teal-300);
}
.scan-line {
position: absolute;
left: 25px;
right: 25px;
top: 139px;
height: 4px;
border-radius: 999px;
background: linear-gradient(90deg, transparent, var(--teal-300), transparent);
box-shadow: 0 0 22px rgba(45, 212, 191, 0.8);
}
.result {
position: absolute;
left: 23px;
right: 23px;
bottom: 25px;
height: 76px;
padding: 14px 15px;
border-radius: 18px;
background: rgba(248, 250, 252, 0.94);
color: var(--navy);
box-shadow: 0 14px 28px rgba(2, 6, 23, 0.22);
}
.result strong {
display: block;
font-size: 18px;
line-height: 1;
font-weight: 850;
}
.result span {
display: block;
margin-top: 8px;
color: #155e63;
font-size: 13px;
line-height: 1.1;
font-weight: 700;
}
</style>
</head>
<body>
<main class="feature">
<section class="grid">
<div class="copy">
<div class="kicker">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#2DD4BF"/>
<path d="M9 12L11.1 14.1L15.5 9.7" stroke="#061525" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Local-first privacy
</div>
<h1>Private QR<br>Scanner</h1>
<p class="subtitle">Scan QR codes and barcodes without ads, tracking, or accounts.</p>
<div class="checks" aria-label="Feature highlights">
<div class="check">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20 6L9 17L4 12" stroke="#061525" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Local
</div>
<div class="check">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20 6L9 17L4 12" stroke="#061525" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
No ads
</div>
<div class="check">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20 6L9 17L4 12" stroke="#061525" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
No tracking
</div>
</div>
</div>
<div class="visual" aria-hidden="true">
<div class="halo"></div>
<div class="phone">
<div class="screen">
<div class="scan-window">
<div class="qr">
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="corner tl"></div>
<div class="corner tr"></div>
<div class="corner bl"></div>
<div class="corner br"></div>
</div>
<div class="scan-line"></div>
<div class="result">
<strong>Ready to scan</strong>
<span>Inspect first</span>
</div>
</div>
</div>
</div>
</section>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

@@ -0,0 +1,515 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 1</title>
<style>
:root {
--bg: #07111f;
--surface: #f8fafc;
--surface-2: #eef7f5;
--ink: #0b1220;
--muted: #5e7282;
--teal: #2dd4bf;
--mint: #dff7f2;
--blue: #1d4ed8;
}
* {
box-sizing: border-box;
}
html,
body {
width: 1080px;
height: 1920px;
margin: 0;
overflow: hidden;
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
background: var(--bg);
color: var(--surface);
}
.shot {
position: relative;
width: 1080px;
height: 1920px;
overflow: hidden;
background:
radial-gradient(circle at 72% 17%, rgba(45, 212, 191, 0.22), transparent 27%),
radial-gradient(circle at 22% 70%, rgba(223, 247, 242, 0.10), transparent 24%),
linear-gradient(180deg, #0b1220 0%, #103840 52%, #07111f 100%);
}
.camera {
position: absolute;
inset: 0;
overflow: hidden;
background:
linear-gradient(90deg, rgba(45, 212, 191, 0.09) 1px, transparent 1px) 0 0 / 68px 68px,
linear-gradient(0deg, rgba(45, 212, 191, 0.08) 1px, transparent 1px) 0 0 / 68px 68px,
radial-gradient(circle at 58% 38%, rgba(45, 212, 191, 0.17), transparent 22%),
linear-gradient(150deg, rgba(8, 17, 31, 0.15), rgba(8, 17, 31, 0.78));
}
.camera::before,
.camera::after {
content: "";
position: absolute;
left: -120px;
right: -120px;
pointer-events: none;
}
.camera::before {
top: 314px;
height: 424px;
background: rgba(223, 247, 242, 0.10);
clip-path: polygon(0 63%, 19% 42%, 38% 48%, 58% 33%, 78% 42%, 100% 21%, 100% 100%, 0 100%);
}
.camera::after {
top: 642px;
height: 470px;
background: rgba(2, 6, 23, 0.58);
clip-path: polygon(0 23%, 18% 10%, 36% 28%, 54% 18%, 73% 31%, 100% 13%, 100% 100%, 0 100%);
}
.status {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 5;
display: flex;
justify-content: space-between;
align-items: center;
height: 92px;
padding: 22px 44px 0;
color: #f8fafc;
font-size: 24px;
font-weight: 760;
}
.system-icons {
display: flex;
align-items: center;
gap: 10px;
}
.signal {
display: flex;
align-items: end;
gap: 4px;
height: 22px;
}
.signal span {
width: 5px;
border-radius: 999px;
background: #f8fafc;
}
.signal span:nth-child(1) { height: 8px; }
.signal span:nth-child(2) { height: 12px; }
.signal span:nth-child(3) { height: 16px; }
.signal span:nth-child(4) { height: 21px; }
.battery {
width: 42px;
height: 20px;
border: 2px solid #f8fafc;
border-radius: 6px;
padding: 3px;
}
.battery::after {
content: "";
display: block;
width: 27px;
height: 10px;
border-radius: 3px;
background: var(--teal);
}
.top-chip {
position: absolute;
top: 118px;
left: 212px;
right: 212px;
z-index: 3;
display: flex;
justify-content: center;
align-items: center;
min-height: 56px;
padding: 11px 18px;
border-radius: 18px;
color: white;
background: rgba(0, 0, 0, 0.45);
font-size: 23px;
font-weight: 700;
text-align: center;
box-shadow: 0 18px 42px rgba(2, 6, 23, 0.24);
}
.gallery {
position: absolute;
top: 112px;
right: 42px;
z-index: 3;
display: grid;
place-items: center;
width: 64px;
height: 64px;
border: 1px solid rgba(248, 250, 252, 0.22);
border-radius: 18px;
background: rgba(0, 0, 0, 0.38);
}
.gallery svg {
width: 32px;
height: 32px;
}
.aim {
position: absolute;
top: 392px;
left: 166px;
z-index: 2;
width: 748px;
height: 480px;
border-radius: 54px;
background: rgba(45, 212, 191, 0.06);
border: 7px solid rgba(45, 212, 191, 0.9);
box-shadow:
inset 0 0 0 2px rgba(223, 247, 242, 0.20),
0 0 72px rgba(45, 212, 191, 0.27);
}
.qr-target {
position: absolute;
top: 478px;
left: 332px;
z-index: 3;
width: 416px;
height: 300px;
border-radius: 26px;
padding: 26px;
background: rgba(248, 250, 252, 0.97);
box-shadow: 0 28px 70px rgba(2, 6, 23, 0.36);
}
.qr-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 13px;
width: 100%;
height: 100%;
}
.qr-grid span {
border-radius: 5px;
background: #07111f;
}
.qr-grid span:nth-child(4),
.qr-grid span:nth-child(10),
.qr-grid span:nth-child(18),
.qr-grid span:nth-child(25),
.qr-grid span:nth-child(32) {
background: var(--teal);
}
.detect-box {
position: absolute;
top: 466px;
left: 314px;
z-index: 4;
width: 452px;
height: 336px;
border: 5px solid #4ae3a3;
border-radius: 32px;
box-shadow: 0 0 0 999px rgba(2, 6, 23, 0.07);
}
.scan-line {
position: absolute;
top: 642px;
left: 214px;
right: 214px;
z-index: 5;
height: 7px;
border-radius: 999px;
background: linear-gradient(90deg, transparent, #2dd4bf, transparent);
box-shadow: 0 0 34px rgba(45, 212, 191, 0.92);
}
.hint {
position: absolute;
left: 238px;
right: 238px;
bottom: 616px;
z-index: 6;
min-height: 58px;
padding: 13px 22px;
border-radius: 24px;
background: rgba(0, 0, 0, 0.45);
color: #f8fafc;
font-size: 25px;
font-weight: 660;
text-align: center;
}
.sheet {
position: absolute;
left: 0;
right: 0;
bottom: 118px;
z-index: 8;
min-height: 612px;
padding: 22px 38px 34px;
border-radius: 42px 42px 0 0;
background: #f8fafc;
color: var(--ink);
box-shadow: 0 -34px 72px rgba(2, 6, 23, 0.35);
}
.handle {
width: 94px;
height: 8px;
margin: 0 auto 26px;
border-radius: 999px;
background: #cbd5e1;
}
.result-card {
padding: 28px;
border-radius: 26px;
background: #f2f7ff;
}
.result-title {
display: flex;
align-items: center;
gap: 14px;
margin: 0 0 24px;
color: #172033;
font-size: 32px;
font-weight: 800;
}
.badge {
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 14px;
border-radius: 999px;
background: var(--mint);
color: #0f766e;
font-size: 18px;
font-weight: 820;
}
.field {
margin-top: 18px;
}
.field-label {
color: #526879;
font-size: 19px;
font-weight: 760;
}
.field-value {
margin-top: 6px;
color: var(--blue);
font-size: 24px;
line-height: 1.28;
font-weight: 640;
text-decoration: underline;
}
.risk {
display: flex;
align-items: center;
gap: 14px;
margin-top: 22px;
padding: 16px 18px;
border-radius: 20px;
background: #ecfdf5;
color: #065f46;
font-size: 22px;
font-weight: 740;
}
.risk svg {
width: 28px;
height: 28px;
flex: 0 0 auto;
}
.actions {
display: flex;
gap: 18px;
margin-top: 26px;
}
.action {
display: grid;
place-items: center;
width: 70px;
height: 70px;
border-radius: 50%;
background: #eef2f7;
color: #1e293b;
}
.action svg {
width: 32px;
height: 32px;
}
.primary {
margin-left: auto;
width: 250px;
border-radius: 22px;
background: #0f766e;
color: #f8fafc;
font-size: 24px;
font-weight: 820;
}
.nav {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
display: grid;
grid-template-columns: repeat(3, 1fr);
height: 118px;
padding: 12px 56px 18px;
background: #fcfffe;
color: #607080;
border-top: 1px solid rgba(15, 23, 42, 0.08);
}
.nav-item {
display: grid;
place-items: center;
align-content: center;
gap: 7px;
font-size: 20px;
font-weight: 740;
}
.nav-item svg {
width: 28px;
height: 28px;
}
.nav-item.active {
color: #0f766e;
}
</style>
</head>
<body>
<main class="shot">
<div class="camera"></div>
<div class="status">
<div>9:41</div>
<div class="system-icons">
<div class="signal"><span></span><span></span><span></span><span></span></div>
<div class="battery"></div>
</div>
</div>
<div class="top-chip">Everyday personal use</div>
<div class="gallery" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
<path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 9.2H15.52" stroke="white" stroke-width="3" stroke-linecap="round"/>
</svg>
</div>
<div class="aim"></div>
<div class="qr-target">
<div class="qr-grid">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<div class="detect-box"></div>
<div class="scan-line"></div>
<div class="hint">Readable code detected.</div>
<section class="sheet">
<div class="handle"></div>
<div class="result-card">
<h1 class="result-title">URL <span class="badge">Local check</span></h1>
<div class="field">
<div class="field-label">Link</div>
<div class="field-value">https://example.org/menu</div>
</div>
<div class="field">
<div class="field-label">Risk score</div>
<div class="field-value" style="color:#0f172a;text-decoration:none">0</div>
</div>
<div class="risk">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#10B981"/>
<path d="M9 12L11.1 14.1L15.5 9.7" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Checked on device before opening
</div>
</div>
<div class="actions">
<div class="action" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="action" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M18 8C19.7 8 21 6.7 21 5C21 3.3 19.7 2 18 2C16.3 2 15 3.3 15 5C15 5.2 15 5.4 15.1 5.6L8.6 9.2C8.1 8.5 7.1 8 6 8C4.3 8 3 9.3 3 11C3 12.7 4.3 14 6 14C7.1 14 8.1 13.5 8.6 12.8L15.1 16.4C15 16.6 15 16.8 15 17C15 18.7 16.3 20 18 20C19.7 20 21 18.7 21 17C21 15.3 19.7 14 18 14C16.9 14 15.9 14.5 15.4 15.2L8.9 11.6C9 11.4 9 11.2 9 11C9 10.8 9 10.6 8.9 10.4L15.4 6.8C15.9 7.5 16.9 8 18 8Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<div class="action primary">Open</div>
</div>
</section>
<nav class="nav">
<div class="nav-item active">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
</svg>
Scan
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
History
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Settings
</div>
</nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

@@ -0,0 +1,525 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 2</title>
<style>
:root {
--bg: #f6fbfa;
--surface: #ffffff;
--surface-2: #edf8f5;
--ink: #0b1220;
--muted: #607080;
--line: #dce8e6;
--teal: #0f766e;
--teal-bright: #2dd4bf;
--mint: #dff7f2;
--blue: #1d4ed8;
}
* {
box-sizing: border-box;
}
html,
body {
width: 1080px;
height: 1920px;
margin: 0;
overflow: hidden;
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
background: var(--bg);
color: var(--ink);
}
.shot {
position: relative;
width: 1080px;
height: 1920px;
overflow: hidden;
background:
radial-gradient(circle at 92% -4%, rgba(45, 212, 191, 0.20), transparent 31%),
linear-gradient(180deg, #f6fbfa 0%, #eff8f6 100%);
}
.status {
display: flex;
justify-content: space-between;
align-items: center;
height: 92px;
padding: 22px 44px 0;
color: var(--ink);
font-size: 24px;
font-weight: 760;
}
.system-icons {
display: flex;
align-items: center;
gap: 10px;
}
.signal {
display: flex;
align-items: end;
gap: 4px;
height: 22px;
}
.signal span {
width: 5px;
border-radius: 999px;
background: var(--ink);
}
.signal span:nth-child(1) { height: 8px; }
.signal span:nth-child(2) { height: 12px; }
.signal span:nth-child(3) { height: 16px; }
.signal span:nth-child(4) { height: 21px; }
.battery {
width: 42px;
height: 20px;
border: 2px solid var(--ink);
border-radius: 6px;
padding: 3px;
}
.battery::after {
content: "";
display: block;
width: 27px;
height: 10px;
border-radius: 3px;
background: var(--teal-bright);
}
.content {
padding: 30px 48px 144px;
}
.app-head {
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 28px;
}
.app-icon {
width: 80px;
height: 80px;
border-radius: 20px;
box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18);
}
.app-title {
margin: 0;
font-size: 34px;
line-height: 1.08;
font-weight: 850;
letter-spacing: 0;
}
.app-subtitle {
margin-top: 5px;
color: var(--muted);
font-size: 20px;
font-weight: 660;
}
.hero {
margin-bottom: 28px;
padding: 28px;
border-radius: 34px;
background:
radial-gradient(circle at 88% 20%, rgba(45, 212, 191, 0.18), transparent 34%),
linear-gradient(145deg, #0b1220, #123b3f);
color: #f8fafc;
box-shadow: 0 22px 44px rgba(7, 17, 31, 0.18);
}
.hero-row {
display: grid;
grid-template-columns: 1fr 152px;
gap: 20px;
align-items: center;
}
.hero h1 {
margin: 0;
font-size: 47px;
line-height: 1.04;
font-weight: 850;
letter-spacing: 0;
}
.hero p {
margin: 16px 0 0;
color: #cbe7e3;
font-size: 25px;
line-height: 1.25;
font-weight: 560;
}
.safe-card {
display: grid;
place-items: center;
width: 152px;
height: 152px;
border-radius: 30px;
background: rgba(223, 247, 242, 0.12);
border: 1px solid rgba(223, 247, 242, 0.18);
}
.safe-card svg {
width: 86px;
height: 86px;
}
.privacy-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 13px;
margin-top: 26px;
}
.privacy-pill {
display: flex;
align-items: center;
justify-content: center;
min-height: 58px;
border-radius: 18px;
background: rgba(223, 247, 242, 0.12);
color: #dff7f2;
font-size: 20px;
font-weight: 820;
text-align: center;
}
.section-title {
margin: 0 0 18px;
color: #132032;
font-size: 31px;
font-weight: 850;
}
.search {
display: flex;
align-items: center;
gap: 14px;
height: 72px;
margin-bottom: 16px;
padding: 0 22px;
border: 2px solid #cfe0de;
border-radius: 20px;
background: var(--surface);
color: var(--muted);
font-size: 22px;
font-weight: 640;
}
.search svg {
width: 26px;
height: 26px;
color: var(--teal);
}
.export-row {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.export {
height: 50px;
padding: 0 18px;
border: 0;
border-radius: 15px;
background: var(--mint);
color: var(--teal);
font-size: 18px;
font-weight: 840;
}
.delete {
margin-left: auto;
background: #fff1f2;
color: #be123c;
}
.history-list {
display: grid;
gap: 14px;
}
.history-item {
display: grid;
grid-template-columns: 58px 1fr;
gap: 16px;
padding: 19px;
border-radius: 24px;
background: var(--surface);
border: 1px solid rgba(15, 23, 42, 0.06);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
}
.type-icon {
display: grid;
place-items: center;
width: 58px;
height: 58px;
border-radius: 16px;
background: var(--surface-2);
color: var(--teal);
}
.type-icon svg {
width: 30px;
height: 30px;
}
.row-top {
display: flex;
justify-content: space-between;
gap: 18px;
align-items: baseline;
}
.type {
color: #132032;
font-size: 24px;
font-weight: 850;
}
.time {
color: #7b8b98;
font-size: 18px;
font-weight: 680;
white-space: nowrap;
}
.value {
margin-top: 7px;
color: #41566a;
font-size: 21px;
line-height: 1.25;
font-weight: 620;
}
.settings-panel {
margin-top: 30px;
padding: 24px;
border-radius: 30px;
background: var(--surface);
box-shadow: 0 18px 38px rgba(15, 23, 42, 0.07);
}
.setting {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 74px;
border-bottom: 1px solid var(--line);
color: #172033;
font-size: 24px;
font-weight: 780;
}
.setting:last-child {
border-bottom: 0;
}
.switch {
position: relative;
width: 70px;
height: 40px;
border-radius: 999px;
background: var(--teal);
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.04);
}
.switch::after {
content: "";
position: absolute;
top: 5px;
right: 5px;
width: 30px;
height: 30px;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 4px 8px rgba(15, 23, 42, 0.24);
}
.nav {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
display: grid;
grid-template-columns: repeat(3, 1fr);
height: 118px;
padding: 12px 56px 18px;
background: #fcfffe;
color: #607080;
border-top: 1px solid rgba(15, 23, 42, 0.08);
}
.nav-item {
display: grid;
place-items: center;
align-content: center;
gap: 7px;
font-size: 20px;
font-weight: 740;
}
.nav-item svg {
width: 28px;
height: 28px;
}
.nav-item.active {
color: var(--teal);
}
</style>
</head>
<body>
<main class="shot">
<div class="status">
<div>9:41</div>
<div class="system-icons">
<div class="signal"><span></span><span></span><span></span><span></span></div>
<div class="battery"></div>
</div>
</div>
<section class="content">
<header class="app-head">
<img class="app-icon" src="./private-qr-scanner-icon.svg" alt="">
<div>
<h1 class="app-title">Private QR Scanner</h1>
<div class="app-subtitle">Optional history stays on your device</div>
</div>
</header>
<section class="hero">
<div class="hero-row">
<div>
<h1>Review past scans locally</h1>
<p>Search, export, or delete saved scans whenever you choose.</p>
</div>
<div class="safe-card" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#2DD4BF"/>
<path d="M8.6 12.2L10.9 14.5L15.8 9.5" stroke="#07111F" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="privacy-strip">
<div class="privacy-pill">No ads</div>
<div class="privacy-pill">No tracking</div>
<div class="privacy-pill">No account</div>
</div>
</section>
<h2 class="section-title">History</h2>
<div class="search">
<svg viewBox="0 0 24 24" fill="none">
<path d="M10.8 18.2C14.9 18.2 18.2 14.9 18.2 10.8C18.2 6.7 14.9 3.4 10.8 3.4C6.7 3.4 3.4 6.7 3.4 10.8C3.4 14.9 6.7 18.2 10.8 18.2Z" stroke="currentColor" stroke-width="2.2"/>
<path d="M16.4 16.4L21 21" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
</svg>
Search saved scans
</div>
<div class="export-row">
<button class="export">TXT</button>
<button class="export">CSV</button>
<button class="export">JSON</button>
<button class="export delete">Delete all</button>
</div>
<div class="history-list">
<article class="history-item">
<div class="type-icon">
<svg viewBox="0 0 24 24" fill="none">
<path d="M10 13A5 5 0 0 0 17.1 13L20 10.1A5 5 0 0 0 12.9 3L11.8 4.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M14 11A5 5 0 0 0 6.9 11L4 13.9A5 5 0 0 0 11.1 21L12.2 19.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div>
<div class="row-top"><div class="type">URL</div><div class="time">Today, 9:38 AM</div></div>
<div class="value">https://example.org/menu</div>
</div>
</article>
<article class="history-item">
<div class="type-icon">
<svg viewBox="0 0 24 24" fill="none">
<path d="M4 8L12 13L20 8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 6H19C19.6 6 20 6.4 20 7V17C20 17.6 19.6 18 19 18H5C4.4 18 4 17.6 4 17V7C4 6.4 4.4 6 5 6Z" stroke="currentColor" stroke-width="2.2"/>
</svg>
</div>
<div>
<div class="row-top"><div class="type">Email</div><div class="time">Today, 9:21 AM</div></div>
<div class="value">support@example.org</div>
</div>
</article>
<article class="history-item">
<div class="type-icon">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
<path d="M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
<path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>
</svg>
</div>
<div>
<div class="row-top"><div class="type">Wi-Fi</div><div class="time">Yesterday</div></div>
<div class="value">SSID: Guest Network</div>
</div>
</article>
</div>
<section class="settings-panel">
<div class="setting">
<span>Save history (local)</span>
<span class="switch"></span>
</div>
<div class="setting">
<span>Security warnings</span>
<span class="switch"></span>
</div>
<div class="setting">
<span>Scan feedback</span>
<span class="switch"></span>
</div>
</section>
</section>
<nav class="nav">
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
</svg>
Scan
</div>
<div class="nav-item active">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
History
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Settings
</div>
</nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

@@ -0,0 +1,576 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 3</title>
<style>
:root {
--deep: #07111f;
--navy: #0b1220;
--teal-900: #123b3f;
--teal-700: #0f766e;
--teal-300: #2dd4bf;
--mint: #dff7f2;
--white: #f8fafc;
--muted: #cbe7e3;
--warning: #ffc857;
--danger: #f43f5e;
}
* {
box-sizing: border-box;
}
html,
body {
width: 1080px;
height: 1920px;
margin: 0;
overflow: hidden;
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
background: var(--deep);
color: var(--white);
}
.shot {
position: relative;
width: 1080px;
height: 1920px;
overflow: hidden;
background:
radial-gradient(circle at 78% 18%, rgba(45, 212, 191, 0.24), transparent 26%),
linear-gradient(180deg, #07111f 0%, #103840 48%, #07111f 100%);
}
.camera {
position: absolute;
inset: 0;
background:
linear-gradient(90deg, rgba(45, 212, 191, 0.10) 1px, transparent 1px) 0 0 / 68px 68px,
linear-gradient(0deg, rgba(45, 212, 191, 0.08) 1px, transparent 1px) 0 0 / 68px 68px,
radial-gradient(circle at 50% 42%, rgba(45, 212, 191, 0.19), transparent 25%),
linear-gradient(150deg, rgba(8, 17, 31, 0.16), rgba(8, 17, 31, 0.82));
}
.camera::before,
.camera::after {
content: "";
position: absolute;
left: -130px;
right: -130px;
pointer-events: none;
}
.camera::before {
top: 326px;
height: 420px;
background: rgba(223, 247, 242, 0.10);
clip-path: polygon(0 56%, 19% 33%, 41% 45%, 61% 28%, 80% 39%, 100% 18%, 100% 100%, 0 100%);
}
.camera::after {
top: 674px;
height: 488px;
background: rgba(2, 6, 23, 0.62);
clip-path: polygon(0 18%, 18% 8%, 36% 27%, 54% 18%, 74% 34%, 100% 16%, 100% 100%, 0 100%);
}
.status {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 5;
display: flex;
justify-content: space-between;
align-items: center;
height: 92px;
padding: 22px 44px 0;
font-size: 24px;
font-weight: 760;
}
.system-icons {
display: flex;
align-items: center;
gap: 10px;
}
.signal {
display: flex;
align-items: end;
gap: 4px;
height: 22px;
}
.signal span {
width: 5px;
border-radius: 999px;
background: var(--white);
}
.signal span:nth-child(1) { height: 8px; }
.signal span:nth-child(2) { height: 12px; }
.signal span:nth-child(3) { height: 16px; }
.signal span:nth-child(4) { height: 21px; }
.battery {
width: 42px;
height: 20px;
border: 2px solid var(--white);
border-radius: 6px;
padding: 3px;
}
.battery::after {
content: "";
display: block;
width: 27px;
height: 10px;
border-radius: 3px;
background: var(--teal-300);
}
.top-chip {
position: absolute;
top: 118px;
left: 212px;
right: 212px;
z-index: 4;
min-height: 56px;
padding: 11px 18px;
border-radius: 18px;
background: rgba(0, 0, 0, 0.52);
text-align: center;
font-size: 23px;
font-weight: 800;
}
.whitelist {
position: absolute;
top: 188px;
left: 276px;
right: 276px;
z-index: 4;
min-height: 44px;
padding: 8px 12px;
border-radius: 15px;
background: rgba(0, 0, 0, 0.40);
color: var(--mint);
text-align: center;
font-size: 18px;
font-weight: 760;
}
.import,
.gallery {
position: absolute;
top: 112px;
z-index: 4;
display: grid;
place-items: center;
width: 64px;
height: 64px;
border: 1px solid rgba(248, 250, 252, 0.22);
border-radius: 18px;
background: rgba(0, 0, 0, 0.38);
}
.import { right: 118px; }
.gallery { right: 42px; opacity: 0.48; }
.import svg,
.gallery svg {
width: 32px;
height: 32px;
}
.mode {
position: absolute;
top: 116px;
left: 42px;
z-index: 4;
width: 104px;
padding: 10px;
border-radius: 18px;
background: rgba(0, 0, 0, 0.38);
text-align: center;
color: var(--white);
font-size: 15px;
font-weight: 760;
}
.mode svg {
display: block;
width: 36px;
height: 36px;
margin: 0 auto 6px;
color: var(--teal-300);
}
.aim {
position: absolute;
top: 388px;
left: 166px;
z-index: 2;
width: 748px;
height: 480px;
border-radius: 54px;
background: rgba(45, 212, 191, 0.07);
border: 7px solid rgba(45, 212, 191, 0.95);
box-shadow:
inset 0 0 0 2px rgba(223, 247, 242, 0.20),
0 0 72px rgba(45, 212, 191, 0.30);
}
.ticket {
position: absolute;
top: 500px;
left: 268px;
z-index: 3;
width: 544px;
height: 248px;
border-radius: 32px;
padding: 28px;
background:
radial-gradient(circle at 0 50%, transparent 26px, var(--white) 27px),
radial-gradient(circle at 100% 50%, transparent 26px, var(--white) 27px),
linear-gradient(90deg, var(--white), #e9fffb);
background-repeat: no-repeat;
box-shadow: 0 28px 70px rgba(2, 6, 23, 0.36);
color: var(--navy);
}
.ticket-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 22px;
}
.event {
font-size: 28px;
line-height: 1;
font-weight: 900;
}
.valid {
padding: 8px 12px;
border-radius: 999px;
background: var(--mint);
color: var(--teal-700);
font-size: 16px;
font-weight: 900;
}
.barcode {
display: grid;
grid-template-columns: repeat(18, 1fr);
gap: 7px;
height: 82px;
margin-bottom: 18px;
}
.barcode span {
border-radius: 3px;
background: var(--navy);
}
.barcode span:nth-child(3n),
.barcode span:nth-child(7),
.barcode span:nth-child(16) {
background: var(--teal-300);
}
.ticket-id {
color: #526879;
font-size: 19px;
font-weight: 800;
}
.detect-box {
position: absolute;
top: 484px;
left: 246px;
z-index: 4;
width: 588px;
height: 282px;
border: 5px solid #4ae3a3;
border-radius: 38px;
}
.scan-line {
position: absolute;
top: 638px;
left: 214px;
right: 214px;
z-index: 5;
height: 7px;
border-radius: 999px;
background: linear-gradient(90deg, transparent, var(--teal-300), transparent);
box-shadow: 0 0 34px rgba(45, 212, 191, 0.92);
}
.hint {
position: absolute;
left: 238px;
right: 238px;
bottom: 616px;
z-index: 6;
min-height: 58px;
padding: 13px 22px;
border-radius: 24px;
background: rgba(0, 0, 0, 0.45);
color: var(--white);
font-size: 25px;
font-weight: 760;
text-align: center;
}
.batch {
position: absolute;
left: 28px;
right: 28px;
bottom: 132px;
z-index: 8;
padding: 22px;
border-radius: 28px;
background: rgba(2, 6, 23, 0.58);
color: var(--white);
box-shadow: 0 -20px 60px rgba(2, 6, 23, 0.36);
backdrop-filter: blur(10px);
}
.batch-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
color: var(--white);
font-size: 26px;
font-weight: 900;
}
.share {
padding: 9px 14px;
border-radius: 999px;
background: var(--mint);
color: var(--teal-700);
font-size: 17px;
font-weight: 900;
}
.capture {
display: grid;
grid-template-columns: 1fr auto;
gap: 14px;
align-items: center;
padding: 14px 0;
border-top: 1px solid rgba(223, 247, 242, 0.14);
}
.capture:first-of-type {
border-top: 0;
}
.capture strong {
display: block;
color: var(--white);
font-size: 21px;
line-height: 1.2;
}
.capture span {
display: block;
margin-top: 4px;
color: rgba(223, 247, 242, 0.72);
font-size: 17px;
font-weight: 680;
}
.copy {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 14px;
color: var(--white);
background: rgba(223, 247, 242, 0.12);
}
.copy svg {
width: 24px;
height: 24px;
}
.warning {
margin-top: 12px;
display: flex;
align-items: center;
gap: 10px;
padding: 13px 14px;
border-radius: 18px;
background: rgba(244, 63, 94, 0.16);
color: #ffe4e6;
font-size: 18px;
font-weight: 800;
}
.warning svg {
width: 24px;
height: 24px;
flex: 0 0 auto;
}
.nav {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
display: grid;
grid-template-columns: repeat(3, 1fr);
height: 118px;
padding: 12px 56px 18px;
background: #fcfffe;
color: #607080;
border-top: 1px solid rgba(15, 23, 42, 0.08);
}
.nav-item {
display: grid;
place-items: center;
align-content: center;
gap: 7px;
font-size: 20px;
font-weight: 740;
}
.nav-item svg {
width: 28px;
height: 28px;
}
.nav-item.active {
color: var(--teal-700);
}
</style>
</head>
<body>
<main class="shot">
<div class="camera"></div>
<div class="status">
<div>9:41</div>
<div class="system-icons">
<div class="signal"><span></span><span></span><span></span><span></span></div>
<div class="battery"></div>
</div>
</div>
<div class="mode" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M4 5H20V19H4V5Z" stroke="currentColor" stroke-width="2"/>
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Batch
</div>
<div class="top-chip">Event &amp; ticketing</div>
<div class="whitelist">Registered IDs loaded: 412</div>
<div class="import" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 3V15M12 3L8 7M12 3L16 7" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 15V19H19V15" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="gallery" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
<path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="aim"></div>
<section class="ticket">
<div class="ticket-top">
<div class="event">Entry Pass</div>
<div class="valid">VALID</div>
</div>
<div class="barcode">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="ticket-id">ID: EVT-24-0187</div>
</section>
<div class="detect-box"></div>
<div class="scan-line"></div>
<div class="hint">Readable code detected.</div>
<section class="batch">
<div class="batch-title">
<div>Batch captures: 3</div>
<div class="share">Share batch</div>
</div>
<div class="capture">
<div><strong>Barcode: EVT-24-0187</strong><span>9:41 AM</span></div>
<div class="copy">
<svg viewBox="0 0 24 24" fill="none">
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
</div>
<div class="capture">
<div><strong>QR Code: EVT-24-0186</strong><span>9:40 AM</span></div>
<div class="copy">
<svg viewBox="0 0 24 24" fill="none">
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
</div>
<div class="capture">
<div><strong>QR Code: EVT-24-0185</strong><span>9:39 AM</span></div>
<div class="copy">
<svg viewBox="0 0 24 24" fill="none">
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
</div>
<div class="warning">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 3L22 20H2L12 3Z" fill="#F43F5E"/>
<path d="M12 9V13M12 17H12.02" stroke="white" stroke-width="2.3" stroke-linecap="round"/>
</svg>
Duplicate and unregistered ticket alerts stay on device.
</div>
</section>
<nav class="nav">
<div class="nav-item active">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
</svg>
Scan
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
History
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Settings
</div>
</nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

@@ -0,0 +1,592 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 4</title>
<style>
:root {
--bg: #f6fbfa;
--surface: #ffffff;
--soft: #f2f7ff;
--ink: #0b1220;
--muted: #607080;
--line: #dce8e6;
--teal: #0f766e;
--teal-300: #2dd4bf;
--mint: #dff7f2;
--navy: #07111f;
}
* {
box-sizing: border-box;
}
html,
body {
width: 1080px;
height: 1920px;
margin: 0;
overflow: hidden;
font-family: Inter, "DejaVu Sans", Arial, sans-serif;
background: var(--bg);
color: var(--ink);
}
.shot {
position: relative;
width: 1080px;
height: 1920px;
overflow: hidden;
background:
radial-gradient(circle at 88% -4%, rgba(45, 212, 191, 0.18), transparent 31%),
linear-gradient(180deg, #f7fcfb 0%, #eef8f6 100%);
}
.status {
display: flex;
justify-content: space-between;
align-items: center;
height: 92px;
padding: 22px 44px 0;
color: var(--ink);
font-size: 24px;
font-weight: 760;
}
.system-icons {
display: flex;
align-items: center;
gap: 10px;
}
.signal {
display: flex;
align-items: end;
gap: 4px;
height: 22px;
}
.signal span {
width: 5px;
border-radius: 999px;
background: var(--ink);
}
.signal span:nth-child(1) { height: 8px; }
.signal span:nth-child(2) { height: 12px; }
.signal span:nth-child(3) { height: 16px; }
.signal span:nth-child(4) { height: 21px; }
.battery {
width: 42px;
height: 20px;
border: 2px solid var(--ink);
border-radius: 6px;
padding: 3px;
}
.battery::after {
content: "";
display: block;
width: 27px;
height: 10px;
border-radius: 3px;
background: var(--teal-300);
}
.content {
padding: 30px 48px 144px;
}
.app-head {
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 28px;
}
.app-icon {
width: 80px;
height: 80px;
border-radius: 20px;
box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18);
}
.app-title {
margin: 0;
font-size: 34px;
line-height: 1.08;
font-weight: 850;
letter-spacing: 0;
}
.app-subtitle {
margin-top: 5px;
color: var(--muted);
font-size: 20px;
font-weight: 660;
}
.hero {
position: relative;
margin-bottom: 28px;
padding: 28px;
border-radius: 34px;
background:
radial-gradient(circle at 84% 26%, rgba(45, 212, 191, 0.20), transparent 34%),
linear-gradient(145deg, #0b1220, #123b3f);
color: #f8fafc;
box-shadow: 0 22px 44px rgba(7, 17, 31, 0.18);
overflow: hidden;
}
.hero::after {
content: "";
position: absolute;
right: -80px;
bottom: -70px;
width: 330px;
height: 230px;
border-radius: 48px;
background: rgba(223, 247, 242, 0.07);
transform: rotate(-10deg);
}
.hero h1 {
position: relative;
z-index: 1;
margin: 0;
width: 720px;
font-size: 49px;
line-height: 1.04;
font-weight: 850;
letter-spacing: 0;
}
.hero p {
position: relative;
z-index: 1;
width: 650px;
margin: 16px 0 0;
color: #cbe7e3;
font-size: 25px;
line-height: 1.25;
font-weight: 560;
}
.chips {
position: relative;
z-index: 1;
display: flex;
gap: 12px;
margin-top: 24px;
}
.chip {
min-height: 56px;
padding: 14px 18px;
border-radius: 18px;
background: rgba(223, 247, 242, 0.12);
color: var(--mint);
font-size: 20px;
font-weight: 850;
}
.section-title {
margin: 0 0 18px;
color: #132032;
font-size: 31px;
font-weight: 850;
}
.image-scan {
display: grid;
grid-template-columns: 1fr 230px;
gap: 18px;
margin-bottom: 28px;
padding: 22px;
border-radius: 28px;
background: var(--surface);
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.07);
}
.image-copy h2 {
margin: 0;
color: #132032;
font-size: 29px;
font-weight: 900;
}
.image-copy p {
margin: 10px 0 0;
color: var(--muted);
font-size: 22px;
line-height: 1.28;
font-weight: 620;
}
.photo {
position: relative;
height: 180px;
border-radius: 24px;
background:
linear-gradient(90deg, rgba(45, 212, 191, 0.11) 1px, transparent 1px) 0 0 / 25px 25px,
linear-gradient(0deg, rgba(45, 212, 191, 0.10) 1px, transparent 1px) 0 0 / 25px 25px,
linear-gradient(145deg, #07111f, #123b3f);
overflow: hidden;
}
.mini-qr {
position: absolute;
left: 50px;
top: 40px;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
width: 130px;
height: 100px;
padding: 10px;
border-radius: 13px;
background: #f8fafc;
transform: rotate(-4deg);
box-shadow: 0 16px 34px rgba(2, 6, 23, 0.28);
}
.mini-qr span {
border-radius: 3px;
background: var(--navy);
}
.mini-qr span:nth-child(5n+3),
.mini-qr span:nth-child(7),
.mini-qr span:nth-child(18) {
background: var(--teal-300);
}
.found {
position: absolute;
inset: 28px 34px;
border: 4px solid var(--teal-300);
border-radius: 20px;
}
.contact-card {
padding: 24px;
border-radius: 30px;
background: linear-gradient(135deg, #081c3b, #0f2e58, #134b73);
color: #f8fafc;
box-shadow: 0 18px 38px rgba(15, 23, 42, 0.16);
}
.contact-top {
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 22px;
}
.initials {
display: grid;
place-items: center;
width: 74px;
height: 74px;
border-radius: 20px;
background: rgba(122, 247, 207, 0.18);
color: #7af7cf;
font-size: 30px;
font-weight: 900;
}
.person {
flex: 1;
min-width: 0;
}
.person h2 {
margin: 0;
font-size: 34px;
line-height: 1.05;
font-weight: 900;
}
.person p {
margin: 7px 0 0;
color: #c7d6e8;
font-size: 21px;
font-weight: 680;
}
.contact-lines {
display: grid;
gap: 13px;
}
.line {
display: grid;
grid-template-columns: 102px 1fr;
gap: 18px;
align-items: baseline;
font-size: 22px;
line-height: 1.26;
}
.line span:first-child {
color: #c7d6e8;
font-size: 17px;
font-weight: 800;
}
.line span:last-child {
color: #f8fafc;
font-weight: 700;
}
.actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin: 18px 0 28px;
}
.action {
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
min-height: 62px;
border-radius: 20px;
background: var(--surface);
color: var(--ink);
font-size: 21px;
font-weight: 850;
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
}
.action.primary {
background: var(--teal);
color: #f8fafc;
}
.action svg {
width: 25px;
height: 25px;
flex: 0 0 auto;
}
.result-grid {
display: grid;
gap: 14px;
}
.result-row {
display: grid;
grid-template-columns: 58px 1fr;
gap: 16px;
padding: 19px;
border-radius: 24px;
background: var(--surface);
border: 1px solid rgba(15, 23, 42, 0.06);
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
}
.type-icon {
display: grid;
place-items: center;
width: 58px;
height: 58px;
border-radius: 16px;
background: rgba(223, 247, 242, 0.65);
color: var(--teal);
}
.type-icon svg {
width: 30px;
height: 30px;
}
.result-row strong {
display: block;
color: #132032;
font-size: 24px;
line-height: 1.12;
font-weight: 900;
}
.result-row span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 21px;
line-height: 1.24;
font-weight: 620;
}
.nav {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 9;
display: grid;
grid-template-columns: repeat(3, 1fr);
height: 118px;
padding: 12px 56px 18px;
background: #fcfffe;
color: #607080;
border-top: 1px solid rgba(15, 23, 42, 0.08);
}
.nav-item {
display: grid;
place-items: center;
align-content: center;
gap: 7px;
font-size: 20px;
font-weight: 740;
}
.nav-item svg {
width: 28px;
height: 28px;
}
.nav-item.active {
color: var(--teal);
}
</style>
</head>
<body>
<main class="shot">
<div class="status">
<div>9:41</div>
<div class="system-icons">
<div class="signal"><span></span><span></span><span></span><span></span></div>
<div class="battery"></div>
</div>
</div>
<section class="content">
<header class="app-head">
<img class="app-icon" src="./private-qr-scanner-icon.svg" alt="">
<div>
<h1 class="app-title">Private QR Scanner</h1>
<div class="app-subtitle">Structured results, practical actions</div>
</div>
</header>
<section class="hero">
<h1>Scan from camera or image</h1>
<p>Recognize contacts, email, Wi-Fi, calendar events, links, and more.</p>
<div class="chips">
<div class="chip">Copy</div>
<div class="chip">Share</div>
<div class="chip">Add contact</div>
</div>
</section>
<section class="image-scan">
<div class="image-copy">
<h2>Scan from image</h2>
<p>Choose a saved photo and pick the detected code you want to use.</p>
</div>
<div class="photo" aria-hidden="true">
<div class="mini-qr">
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="found"></div>
</div>
</section>
<h2 class="section-title">Contact result</h2>
<section class="contact-card">
<div class="contact-top">
<div class="initials">AR</div>
<div class="person">
<h2>Avery Reed</h2>
<p>Operations Lead • North Hall Events</p>
</div>
</div>
<div class="contact-lines">
<div class="line"><span>Phone</span><span>+1 555 0134</span></div>
<div class="line"><span>Email</span><span>avery@example.org</span></div>
<div class="line"><span>Address</span><span>240 Market Street, Suite 8</span></div>
</div>
</section>
<section class="actions">
<div class="action primary">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 12C14.2 12 16 10.2 16 8C16 5.8 14.2 4 12 4C9.8 4 8 5.8 8 8C8 10.2 9.8 12 12 12Z" stroke="currentColor" stroke-width="2"/>
<path d="M4 21C4.7 17.6 7.8 15 12 15C16.2 15 19.3 17.6 20 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M19 5V11M16 8H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Add contact
</div>
<div class="action">
<svg viewBox="0 0 24 24" fill="none">
<path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/>
</svg>
Copy
</div>
<div class="action">
<svg viewBox="0 0 24 24" fill="none">
<path d="M18 8C19.7 8 21 6.7 21 5C21 3.3 19.7 2 18 2C16.3 2 15 3.3 15 5C15 5.2 15 5.4 15.1 5.6L8.6 9.2C8.1 8.5 7.1 8 6 8C4.3 8 3 9.3 3 11C3 12.7 4.3 14 6 14C7.1 14 8.1 13.5 8.6 12.8L15.1 16.4C15 16.6 15 16.8 15 17C15 18.7 16.3 20 18 20C19.7 20 21 18.7 21 17C21 15.3 19.7 14 18 14C16.9 14 15.9 14.5 15.4 15.2L8.9 11.6C9 11.4 9 11.2 9 11C9 10.8 9 10.6 8.9 10.4L15.4 6.8C15.9 7.5 16.9 8 18 8Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
Share
</div>
</section>
<section class="result-grid">
<article class="result-row">
<div class="type-icon">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
<path d="M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
<path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>
</svg>
</div>
<div><strong>Wi-Fi QR codes</strong><span>Open Wi-Fi settings after inspecting network details.</span></div>
</article>
<article class="result-row">
<div class="type-icon">
<svg viewBox="0 0 24 24" fill="none">
<path d="M7 4V7M17 4V7M5 9H19M6 6H18C18.6 6 19 6.4 19 7V19C19 19.6 18.6 20 18 20H6C5.4 20 5 19.6 5 19V7C5 6.4 5.4 6 6 6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div><strong>Calendar events</strong><span>Review title, location, and time before adding.</span></div>
</article>
</section>
</section>
<nav class="nav">
<div class="nav-item active">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
<path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>
</svg>
Scan
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/>
<path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
History
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/>
<path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Settings
</div>
</nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

@@ -0,0 +1,58 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 5</title>
<style>
*{box-sizing:border-box} html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#07111f;color:#f8fafc}
.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 70% 22%,rgba(45,212,191,.22),transparent 26%),linear-gradient(180deg,#07111f,#103840 50%,#07111f)}
.camera{position:absolute;inset:0;background:linear-gradient(90deg,rgba(45,212,191,.09) 1px,transparent 1px) 0 0/68px 68px,linear-gradient(0deg,rgba(45,212,191,.08) 1px,transparent 1px) 0 0/68px 68px,radial-gradient(circle at 50% 39%,rgba(45,212,191,.16),transparent 24%),linear-gradient(150deg,rgba(8,17,31,.18),rgba(8,17,31,.82))}
.camera:before,.camera:after{content:"";position:absolute;left:-120px;right:-120px}.camera:before{top:330px;height:420px;background:rgba(223,247,242,.1);clip-path:polygon(0 60%,19% 34%,40% 45%,61% 29%,80% 41%,100% 18%,100% 100%,0 100%)}.camera:after{top:680px;height:480px;background:rgba(2,6,23,.62);clip-path:polygon(0 18%,20% 8%,38% 27%,56% 17%,75% 33%,100% 14%,100% 100%,0 100%)}
.status{position:absolute;z-index:8;left:0;right:0;top:0;height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#f8fafc}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #f8fafc;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
.chip{position:absolute;z-index:5;top:118px;left:212px;right:212px;min-height:56px;padding:11px 18px;border-radius:18px;background:rgba(0,0,0,.52);text-align:center;font-size:23px;font-weight:800}.gallery{position:absolute;z-index:5;right:42px;top:112px;display:grid;place-items:center;width:64px;height:64px;border-radius:18px;border:1px solid rgba(248,250,252,.22);background:rgba(0,0,0,.38)}.gallery svg{width:32px;height:32px}
.aim{position:absolute;z-index:2;top:390px;left:166px;width:748px;height:480px;border-radius:54px;background:rgba(45,212,191,.06);border:7px solid rgba(45,212,191,.95);box-shadow:0 0 72px rgba(45,212,191,.3),inset 0 0 0 2px rgba(223,247,242,.2)}
.qr{position:absolute;z-index:4;top:478px;left:332px;width:416px;height:300px;border-radius:26px;padding:26px;background:rgba(248,250,252,.97);box-shadow:0 28px 70px rgba(2,6,23,.36);display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(5,1fr);gap:13px}.qr span{border-radius:5px;background:#07111f}.qr span:nth-child(4),.qr span:nth-child(10),.qr span:nth-child(18),.qr span:nth-child(25),.qr span:nth-child(32){background:#2dd4bf}
.detect{position:absolute;z-index:5;top:466px;left:314px;width:452px;height:336px;border:5px solid #4ae3a3;border-radius:32px}.line{position:absolute;z-index:6;top:642px;left:214px;right:214px;height:7px;border-radius:99px;background:linear-gradient(90deg,transparent,#2dd4bf,transparent);box-shadow:0 0 34px rgba(45,212,191,.92)}
.hint{position:absolute;z-index:6;left:238px;right:238px;bottom:632px;padding:13px 22px;border-radius:24px;background:rgba(0,0,0,.45);font-size:25px;font-weight:760;text-align:center}
.sheet{position:absolute;left:0;right:0;bottom:118px;z-index:7;min-height:568px;padding:22px 38px 34px;border-radius:42px 42px 0 0;background:#f8fafc;color:#0b1220;box-shadow:0 -34px 72px rgba(2,6,23,.35)}.handle{width:94px;height:8px;margin:0 auto 26px;border-radius:99px;background:#cbd5e1}
.card{padding:28px;border-radius:26px;background:#f2f7ff}.title{display:flex;gap:14px;align-items:center;margin:0 0 22px;font-size:32px;font-weight:900}.badge{padding:8px 14px;border-radius:99px;background:#fff1f2;color:#be123c;font-size:18px;font-weight:900}.label{margin-top:18px;color:#526879;font-size:19px;font-weight:760}.value{margin-top:6px;color:#1d4ed8;font-size:24px;line-height:1.28;font-weight:700;text-decoration:underline}.score{color:#be123c;text-decoration:none}
.warn-pill{display:flex;gap:12px;align-items:center;margin-top:22px;padding:16px 18px;border-radius:20px;background:#fff1f2;color:#be123c;font-size:22px;font-weight:800}.warn-pill svg{width:28px;height:28px}
.modal-shade{position:absolute;inset:0;z-index:10;background:rgba(2,6,23,.48)}.dialog{position:absolute;z-index:11;left:70px;right:70px;top:730px;padding:30px;border-radius:28px;background:#fff;color:#0b1220;box-shadow:0 30px 80px rgba(2,6,23,.42)}.dialog h2{margin:0 0 14px;font-size:32px;font-weight:900}.dialog p{margin:0;color:#526879;font-size:24px;line-height:1.3;font-weight:620}.dialog-actions{display:flex;justify-content:flex-end;gap:14px;margin-top:28px}.btn{padding:15px 20px;border-radius:18px;font-size:22px;font-weight:900;color:#0f766e}.btn.danger{background:#0f766e;color:#fff}
.nav{position:absolute;z-index:12;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active{color:#0f766e}
</style>
</head>
<body>
<main class="shot">
<div class="camera"></div>
<div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
<div class="chip">Everyday personal use</div>
<div class="gallery"><svg viewBox="0 0 24 24" fill="none"><path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2"/><path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div class="aim"></div>
<div class="qr">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="detect"></div><div class="line"></div><div class="hint">Readable code detected.</div>
<section class="sheet">
<div class="handle"></div>
<div class="card">
<h1 class="title">URL <span class="badge">Unusual link</span></h1>
<div class="label">Link</div><div class="value">https://login.example-secure.co/session</div>
<div class="label">Risk score</div><div class="value score">4</div>
<div class="warn-pill"><svg viewBox="0 0 24 24" fill="none"><path d="M12 3L22 20H2L12 3Z" fill="#F43F5E"/><path d="M12 9V13M12 17H12.02" stroke="white" stroke-width="2.3" stroke-linecap="round"/></svg>Checked locally before opening</div>
</div>
</section>
<div class="modal-shade"></div>
<section class="dialog">
<h2>This URL looks unusual</h2>
<p>Check the link before opening. The warning is calculated on your device, without sending the scan anywhere.</p>
<div class="dialog-actions"><div class="btn">Cancel</div><div class="btn danger">Open anyway</div></div>
</section>
<nav class="nav">
<div class="nav-item active"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div>
<div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div>
<div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div>
</nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 6</title>
<style>
*{box-sizing:border-box}html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#f6fbfa;color:#0b1220}.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 90% -4%,rgba(45,212,191,.18),transparent 31%),linear-gradient(180deg,#f7fcfb,#eef8f6)}
.status{height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#0b1220}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #0b1220;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
.content{padding:30px 48px 144px}.head{display:flex;gap:18px;align-items:center;margin-bottom:28px}.icon{width:80px;height:80px;border-radius:20px;box-shadow:0 14px 28px rgba(15,118,110,.18)}.title{margin:0;font-size:34px;line-height:1.08;font-weight:900}.sub{margin-top:5px;color:#607080;font-size:20px;font-weight:700}
.hero{margin-bottom:28px;padding:30px;border-radius:34px;background:radial-gradient(circle at 88% 24%,rgba(45,212,191,.18),transparent 34%),linear-gradient(145deg,#0b1220,#123b3f);color:#f8fafc;box-shadow:0 22px 44px rgba(7,17,31,.18)}.hero h1{margin:0;font-size:49px;line-height:1.04;font-weight:900}.hero p{margin:16px 0 0;color:#cbe7e3;font-size:25px;line-height:1.25;font-weight:600}.chips{display:flex;gap:12px;margin-top:24px}.chip{padding:14px 18px;border-radius:18px;background:rgba(223,247,242,.12);color:#dff7f2;font-size:20px;font-weight:850}
.wifi-card{padding:28px;border-radius:30px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.08);margin-bottom:20px}.wifi-head{display:flex;gap:18px;align-items:center;margin-bottom:24px}.wifi-icon{display:grid;place-items:center;width:76px;height:76px;border-radius:22px;background:#dff7f2;color:#0f766e}.wifi-icon svg{width:42px;height:42px}.wifi-head h2{margin:0;font-size:38px;font-weight:900}.wifi-head p{margin:5px 0 0;color:#607080;font-size:21px;font-weight:700}.field{padding:18px 0;border-top:1px solid #dce8e6}.field:first-of-type{border-top:0}.field label{display:block;color:#607080;font-size:18px;font-weight:850}.field div{margin-top:7px;color:#0b1220;font-size:26px;font-weight:800}.password{display:flex;justify-content:space-between;align-items:center}.dots{letter-spacing:4px}
.button{display:flex;align-items:center;justify-content:center;gap:12px;min-height:70px;margin:20px 0 28px;border-radius:22px;background:#0f766e;color:#f8fafc;font-size:24px;font-weight:900}.button svg{width:28px;height:28px}
.note{display:flex;gap:14px;align-items:flex-start;padding:22px;border-radius:26px;background:#ecfdf5;color:#065f46;font-size:22px;line-height:1.32;font-weight:750;box-shadow:0 14px 30px rgba(15,23,42,.05)}.note svg{width:30px;height:30px;flex:0 0 auto}
.list{display:grid;gap:14px;margin-top:28px}.row{display:grid;grid-template-columns:58px 1fr;gap:16px;padding:19px;border-radius:24px;background:#fff;box-shadow:0 14px 30px rgba(15,23,42,.06)}.row-icon{display:grid;place-items:center;width:58px;height:58px;border-radius:16px;background:rgba(223,247,242,.65);color:#0f766e}.row-icon svg{width:30px;height:30px}.row strong{display:block;color:#132032;font-size:24px;font-weight:900}.row span{display:block;margin-top:6px;color:#607080;font-size:21px;line-height:1.24;font-weight:620}
.nav{position:absolute;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active{color:#0f766e}
</style>
</head>
<body>
<main class="shot">
<div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
<section class="content">
<header class="head"><img class="icon" src="./private-qr-scanner-icon.svg" alt=""><div><h1 class="title">Private QR Scanner</h1><div class="sub">Structured QR details before acting</div></div></header>
<section class="hero"><h1>Wi-Fi codes stay readable</h1><p>Inspect network name and security type before opening Android Wi-Fi settings.</p><div class="chips"><div class="chip">Local parsing</div><div class="chip">No tracking</div><div class="chip">No account</div></div></section>
<section class="wifi-card">
<div class="wifi-head"><div class="wifi-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/></svg></div><div><h2>Wi-Fi</h2><p>Scanned result</p></div></div>
<div class="field"><label>SSID</label><div>Guest Network</div></div>
<div class="field"><label>Security</label><div>WPA/WPA2</div></div>
<div class="field password"><div><label>Password</label><div class="dots">••••••••••</div></div><div style="color:#0f766e;font-size:20px;font-weight:900">Hidden</div></div>
</section>
<div class="button"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12.5C8.9 8.7 15.1 8.7 19 12.5M8.2 15.4C10.3 13.4 13.7 13.4 15.8 15.4" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M12 19H12.02" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/></svg>Open Wi-Fi settings</div>
<div class="note"><svg viewBox="0 0 24 24" fill="none"><path d="M12 3L19 6V11C19 15.7 16.1 19.9 12 21.8C7.9 19.9 5 15.7 5 11V6L12 3Z" fill="#10B981"/><path d="M9 12L11.1 14.1L15.5 9.7" stroke="white" stroke-width="2.2" stroke-linecap="round"/></svg><div>Parsing happens on the device. Data leaves only when you choose an external action.</div></div>
<div class="list">
<div class="row"><div class="row-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M8 8H6C5.4 8 5 8.4 5 9V19C5 19.6 5.4 20 6 20H16C16.6 20 17 19.6 17 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M8 4H18C18.6 4 19 4.4 19 5V15C19 15.6 18.6 16 18 16H8C7.4 16 7 15.6 7 15V5C7 4.4 7.4 4 8 4Z" stroke="currentColor" stroke-width="2"/></svg></div><div><strong>Copy result</strong><span>Save the raw QR payload when needed.</span></div></div>
<div class="row"><div class="row-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M18 8C19.7 8 21 6.7 21 5C21 3.3 19.7 2 18 2C16.3 2 15 3.3 15 5C15 5.2 15 5.4 15.1 5.6L8.6 9.2C8.1 8.5 7.1 8 6 8C4.3 8 3 9.3 3 11C3 12.7 4.3 14 6 14C7.1 14 8.1 13.5 8.6 12.8L15.1 16.4C15 16.6 15 16.8 15 17C15 18.7 16.3 20 18 20C19.7 20 21 18.7 21 17C21 15.3 19.7 14 18 14C16.9 14 15.9 14.5 15.4 15.2L8.9 11.6C9 11.4 9 11.2 9 11C9 10.8 9 10.6 8.9 10.4L15.4 6.8C15.9 7.5 16.9 8 18 8Z" stroke="currentColor" stroke-width="2"/></svg></div><div><strong>Share deliberately</strong><span>Send scanned content only when you choose.</span></div></div>
</div>
</section>
<nav class="nav"><div class="nav-item active"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div></nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 7</title>
<style>
*{box-sizing:border-box}html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#07111f;color:#f8fafc}.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 72% 18%,rgba(45,212,191,.22),transparent 28%),linear-gradient(180deg,#07111f,#103840 52%,#07111f)}
.grid{position:absolute;inset:0;background:linear-gradient(90deg,rgba(45,212,191,.09) 1px,transparent 1px) 0 0/68px 68px,linear-gradient(0deg,rgba(45,212,191,.08) 1px,transparent 1px) 0 0/68px 68px}.status{position:absolute;z-index:8;left:0;right:0;top:0;height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#f8fafc}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #f8fafc;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
.top{position:absolute;z-index:4;top:118px;left:212px;right:212px;min-height:56px;padding:11px 18px;border-radius:18px;background:rgba(0,0,0,.52);text-align:center;font-size:23px;font-weight:800}.gallery{position:absolute;z-index:5;right:42px;top:112px;display:grid;place-items:center;width:64px;height:64px;border-radius:18px;border:1px solid rgba(248,250,252,.22);background:rgba(0,0,0,.38)}.gallery svg{width:32px;height:32px}
.phone-photo{position:absolute;z-index:2;left:98px;right:98px;top:292px;height:710px;border-radius:38px;background:linear-gradient(150deg,#0b1220,#155e63);box-shadow:0 28px 70px rgba(2,6,23,.36);overflow:hidden}.phone-photo:before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 30% 30%,rgba(223,247,242,.12),transparent 26%),radial-gradient(circle at 82% 68%,rgba(45,212,191,.18),transparent 30%)}.paper{position:absolute;left:134px;top:160px;width:520px;height:350px;border-radius:30px;background:#f8fafc;transform:rotate(-4deg);box-shadow:0 26px 60px rgba(2,6,23,.4)}.paper h2{margin:34px 34px 18px;color:#0b1220;font-size:34px;font-weight:900}.mini{position:absolute;left:320px;top:118px;width:170px;height:145px;border-radius:18px;background:#edf8f6;padding:15px;display:grid;grid-template-columns:repeat(5,1fr);gap:8px}.mini span{border-radius:4px;background:#07111f}.mini span:nth-child(5n+3),.mini span:nth-child(7),.mini span:nth-child(18){background:#2dd4bf}.bars{position:absolute;left:40px;right:40px;bottom:50px;display:grid;gap:14px}.bars span{height:18px;border-radius:99px;background:#cbd5e1}.bars span:nth-child(2){width:70%}.found{position:absolute;left:286px;top:84px;width:230px;height:210px;border:6px solid #2dd4bf;border-radius:28px}.found.two{left:80px;top:196px;width:260px;height:126px;border-color:#ffc857}
.dialog{position:absolute;z-index:7;left:42px;right:42px;bottom:146px;padding:28px;border-radius:34px;background:#fff;color:#0b1220;box-shadow:0 -24px 80px rgba(2,6,23,.4)}.dialog h1{margin:0;font-size:34px;font-weight:900}.dialog p{margin:10px 0 22px;color:#607080;font-size:23px;line-height:1.3;font-weight:650}.preview{height:240px;border-radius:28px;background:linear-gradient(145deg,#07111f,#123b3f);position:relative;overflow:hidden;margin-bottom:20px}.preview .mini{left:595px;top:55px;transform:rotate(-4deg);width:170px;height:140px}.preview .found{left:570px;top:36px;width:220px;height:180px}.candidate{display:grid;grid-template-columns:54px 1fr;gap:15px;align-items:center;padding:18px;border-radius:24px;background:#f6fbfa;margin-top:12px;border:2px solid transparent}.candidate.active{border-color:#2dd4bf;background:#ecfdf5}.candidate-icon{display:grid;place-items:center;width:54px;height:54px;border-radius:16px;background:#dff7f2;color:#0f766e}.candidate-icon svg{width:30px;height:30px}.candidate strong{display:block;font-size:23px;font-weight:900}.candidate span{display:block;margin-top:5px;color:#607080;font-size:20px;line-height:1.2;font-weight:650}.actions{display:flex;justify-content:flex-end;gap:14px;margin-top:24px}.btn{padding:15px 20px;border-radius:18px;font-size:22px;font-weight:900;color:#0f766e}.btn.primary{background:#0f766e;color:#fff}
.nav{position:absolute;z-index:9;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active{color:#0f766e}
</style>
</head>
<body>
<main class="shot">
<div class="grid"></div><div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
<div class="top">Scan from image</div><div class="gallery"><svg viewBox="0 0 24 24" fill="none"><path d="M4 5H20V19H4V5Z" stroke="white" stroke-width="2"/><path d="M7 16L10.6 12.4L13 14.8L15 12.8L19 16.8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<section class="phone-photo"><div class="paper"><h2>Meeting flyer</h2><div class="mini"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div><div class="bars"><span></span><span></span><span></span></div><div class="found"></div><div class="found two"></div></div></section>
<section class="dialog">
<h1>Found 2 codes in image</h1><p>Choose the result you want to use. Detection and parsing happen locally.</p>
<div class="preview"><div class="mini"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div><div class="found"></div></div>
<div class="candidate active"><div class="candidate-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M10 13A5 5 0 0 0 17.1 13L20 10.1A5 5 0 0 0 12.9 3L11.8 4.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M14 11A5 5 0 0 0 6.9 11L4 13.9A5 5 0 0 0 11.1 21L12.2 19.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></div><div><strong>URL</strong><span>https://example.org/register</span></div></div>
<div class="candidate"><div class="candidate-icon"><svg viewBox="0 0 24 24" fill="none"><path d="M4 8L12 13L20 8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M5 6H19C19.6 6 20 6.4 20 7V17C20 17.6 19.6 18 19 18H5C4.4 18 4 17.6 4 17V7C4 6.4 4.4 6 5 6Z" stroke="currentColor" stroke-width="2.2"/></svg></div><div><strong>Email</strong><span>hello@example.org</span></div></div>
<div class="actions"><div class="btn">Cancel</div><div class="btn primary">Use selected</div></div>
</section>
<nav class="nav"><div class="nav-item active"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div></nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1080, height=1920, initial-scale=1">
<title>Private QR Scanner Screenshot 8</title>
<style>
*{box-sizing:border-box}html,body{width:1080px;height:1920px;margin:0;overflow:hidden;font-family:Inter,"DejaVu Sans",Arial,sans-serif;background:#f6fbfa;color:#0b1220}.shot{position:relative;width:1080px;height:1920px;overflow:hidden;background:radial-gradient(circle at 92% -4%,rgba(45,212,191,.2),transparent 31%),linear-gradient(180deg,#f6fbfa,#eff8f6)}
.status{height:92px;padding:22px 44px 0;display:flex;justify-content:space-between;align-items:center;font-size:24px;font-weight:760}.icons{display:flex;gap:10px;align-items:center}.signal{display:flex;gap:4px;align-items:end;height:22px}.signal span{width:5px;border-radius:99px;background:#0b1220}.signal span:nth-child(1){height:8px}.signal span:nth-child(2){height:12px}.signal span:nth-child(3){height:16px}.signal span:nth-child(4){height:21px}.battery{width:42px;height:20px;border:2px solid #0b1220;border-radius:6px;padding:3px}.battery:after{content:"";display:block;width:27px;height:10px;border-radius:3px;background:#2dd4bf}
.content{padding:30px 48px 144px}.head{display:flex;gap:18px;align-items:center;margin-bottom:28px}.icon{width:80px;height:80px;border-radius:20px;box-shadow:0 14px 28px rgba(15,118,110,.18)}.title{margin:0;font-size:34px;line-height:1.08;font-weight:900}.sub{margin-top:5px;color:#607080;font-size:20px;font-weight:700}
.hero{margin-bottom:28px;padding:30px;border-radius:34px;background:radial-gradient(circle at 88% 24%,rgba(45,212,191,.18),transparent 34%),linear-gradient(145deg,#0b1220,#123b3f);color:#f8fafc;box-shadow:0 22px 44px rgba(7,17,31,.18)}.hero h1{margin:0;font-size:49px;line-height:1.04;font-weight:900}.hero p{margin:16px 0 0;color:#cbe7e3;font-size:25px;line-height:1.25;font-weight:600}
.panel{padding:8px 24px;border-radius:30px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.07);margin-bottom:22px}.setting{display:flex;justify-content:space-between;align-items:center;min-height:86px;border-top:1px solid #dce8e6;font-size:25px;font-weight:850}.setting:first-child{border-top:0}.switch{position:relative;width:70px;height:40px;border-radius:99px;background:#0f766e}.switch:after{content:"";position:absolute;right:5px;top:5px;width:30px;height:30px;border-radius:50%;background:#fff;box-shadow:0 4px 8px rgba(15,23,42,.24)}
.section-title{margin:0 0 14px;color:#132032;font-size:31px;font-weight:900}.usecase{padding:24px;border-radius:28px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.07);margin-bottom:22px}.usecase strong{display:block;font-size:25px;font-weight:900}.usecase span{display:block;margin-top:8px;color:#607080;font-size:22px;font-weight:650}.choose{display:inline-flex;margin-top:16px;padding:13px 18px;border-radius:18px;background:#dff7f2;color:#0f766e;font-size:20px;font-weight:900}
.about{display:grid;gap:12px;padding:24px;border-radius:28px;background:#fff;box-shadow:0 18px 38px rgba(15,23,42,.07);color:#607080;font-size:21px;font-weight:650}.about strong{color:#0b1220;font-size:25px;font-weight:900}.links{display:flex;gap:12px;margin-top:8px}.link{padding:13px 16px;border-radius:18px;background:#dff7f2;color:#0f766e;font-size:19px;font-weight:900}
.shade{position:absolute;inset:0;background:rgba(2,6,23,.38);z-index:6}.dialog{position:absolute;left:70px;right:70px;top:665px;z-index:7;padding:30px;border-radius:30px;background:#fff;color:#0b1220;box-shadow:0 30px 80px rgba(2,6,23,.42)}.dialog h2{margin:0 0 18px;font-size:32px;font-weight:900}.option{display:grid;grid-template-columns:58px 1fr;gap:16px;align-items:center;padding:18px;border-radius:24px;background:#f6fbfa;margin-top:12px;border:2px solid transparent}.option.active{border-color:#2dd4bf;background:#ecfdf5}.check{display:grid;place-items:center;width:58px;height:58px;border-radius:16px;background:#dff7f2;color:#0f766e;font-size:30px;font-weight:900}.option strong{display:block;font-size:23px;font-weight:900}.option span{display:block;margin-top:5px;color:#607080;font-size:19px;line-height:1.2;font-weight:650}.actions{display:flex;justify-content:flex-end;margin-top:24px}.btn{padding:15px 20px;border-radius:18px;color:#0f766e;font-size:22px;font-weight:900}
.nav{position:absolute;z-index:9;left:0;right:0;bottom:0;height:118px;padding:12px 56px 18px;display:grid;grid-template-columns:repeat(3,1fr);background:#fcfffe;color:#607080;border-top:1px solid rgba(15,23,42,.08)}.nav-item{display:grid;place-items:center;align-content:center;gap:7px;font-size:20px;font-weight:740}.nav-item svg{width:28px;height:28px}.active-nav{color:#0f766e}
</style>
</head>
<body>
<main class="shot">
<div class="status"><div>9:41</div><div class="icons"><div class="signal"><span></span><span></span><span></span><span></span></div><div class="battery"></div></div></div>
<section class="content">
<header class="head"><img class="icon" src="./private-qr-scanner-icon.svg" alt=""><div><h1 class="title">Private QR Scanner</h1><div class="sub">Settings that keep you in control</div></div></header>
<section class="hero"><h1>Privacy settings are yours</h1><p>Choose local history, security warnings, feedback, and the scanner view that fits your workflow.</p></section>
<section class="panel"><div class="setting"><span>Save history (local)</span><span class="switch"></span></div><div class="setting"><span>Security warnings</span><span class="switch"></span></div><div class="setting"><span>Scan feedback</span><span class="switch"></span></div></section>
<h2 class="section-title">Active use-case view</h2>
<section class="usecase"><strong>Everyday personal use</strong><span>For menus, links, Wi-Fi, contacts, calendar codes, and common daily scans.</span><div class="choose">Select use-case view</div></section>
<section class="about"><strong>About</strong><span>Version 1.0.0</span><span>Contact: softwareapp.hb@gmail.com</span><div class="links"><div class="link">Privacy Policy</div><div class="link">Review on Google Play</div></div></section>
</section>
<div class="shade"></div>
<section class="dialog">
<h2>Select use-case view</h2>
<div class="option active"><div class="check"></div><div><strong>Everyday personal use</strong><span>Full personal scanner with local history and common result actions.</span></div></div>
<div class="option"><div class="check"></div><div><strong>Event &amp; ticketing</strong><span>Batch scanning, duplicate detection, whitelist import, and batch sharing.</span></div></div>
<div class="actions"><div class="btn">Cancel</div></div>
</section>
<nav class="nav"><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 7V5H9M15 5H19V9M19 15V19H15M9 19H5V15" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Scan</div><div class="nav-item"><svg viewBox="0 0 24 24" fill="none"><path d="M5 5H19V19H5V5Z" stroke="currentColor" stroke-width="2"/><path d="M8 9H16M8 13H16M8 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>History</div><div class="nav-item active-nav"><svg viewBox="0 0 24 24" fill="none"><path d="M12 15.5C13.9 15.5 15.5 13.9 15.5 12C15.5 10.1 13.9 8.5 12 8.5C10.1 8.5 8.5 10.1 8.5 12C8.5 13.9 10.1 15.5 12 15.5Z" stroke="currentColor" stroke-width="2"/><path d="M19 12H21M3 12H5M12 3V5M12 19V21M17 7L18.4 5.6M5.6 18.4L7 17M17 17L18.4 18.4M5.6 5.6L7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>Settings</div></nav>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB