nicer visual aperance

This commit is contained in:
Hadrian Burkhardt
2026-05-10 10:31:51 +02:00
parent 120d1672a3
commit 1eb389ea5e
12 changed files with 1821 additions and 171 deletions
@@ -2,8 +2,14 @@ package de.softwareapp_hb.privateqrscanner.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding 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.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.HistoryScreen
import de.softwareapp_hb.privateqrscanner.ui.screens.ScannerScreen import de.softwareapp_hb.privateqrscanner.ui.screens.ScannerScreen
import de.softwareapp_hb.privateqrscanner.ui.screens.SettingsScreen import de.softwareapp_hb.privateqrscanner.ui.screens.SettingsScreen
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
private enum class RootTab { Scanner, History, Settings } private enum class RootTab { Scanner, History, Settings }
@@ -33,8 +40,16 @@ fun CleanScannerAppRoot(container: AppContainer) {
var activeTab by remember { mutableStateOf(RootTab.Scanner) } var activeTab by remember { mutableStateOf(RootTab.Scanner) }
Scaffold( Scaffold(
containerColor = PrivateQrColors.AppBackground,
bottomBar = { 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( NavigationBarItem(
selected = activeTab == RootTab.Scanner, selected = activeTab == RootTab.Scanner,
onClick = { onClick = {
@@ -42,7 +57,8 @@ fun CleanScannerAppRoot(container: AppContainer) {
scannerViewModel.resumeScanning() scannerViewModel.resumeScanning()
}, },
label = { Text(stringResource(R.string.scan)) }, label = { Text(stringResource(R.string.scan)) },
icon = {} icon = { Icon(Icons.Default.QrCodeScanner, contentDescription = null) },
colors = navColors
) )
NavigationBarItem( NavigationBarItem(
selected = activeTab == RootTab.History, selected = activeTab == RootTab.History,
@@ -50,7 +66,8 @@ fun CleanScannerAppRoot(container: AppContainer) {
activeTab = RootTab.History activeTab = RootTab.History
}, },
label = { Text(stringResource(R.string.history)) }, label = { Text(stringResource(R.string.history)) },
icon = {} icon = { Icon(Icons.Default.History, contentDescription = null) },
colors = navColors
) )
NavigationBarItem( NavigationBarItem(
selected = activeTab == RootTab.Settings, selected = activeTab == RootTab.Settings,
@@ -58,7 +75,8 @@ fun CleanScannerAppRoot(container: AppContainer) {
activeTab = RootTab.Settings activeTab = RootTab.Settings
}, },
label = { Text(stringResource(R.string.settings)) }, label = { Text(stringResource(R.string.settings)) },
icon = {} icon = { Icon(Icons.Default.Settings, contentDescription = null) },
colors = navColors
) )
} }
} }
@@ -47,7 +47,8 @@ fun UseCaseView.capabilities(): UseCaseCapabilities {
allowBatchMode = false, allowBatchMode = false,
allowCopy = true, allowCopy = true,
allowShare = true, allowShare = true,
allowOpenUrl = true allowOpenUrl = true,
allowHistoryExport = true
) )
UseCaseView.EventTicketing -> UseCaseCapabilities( UseCaseView.EventTicketing -> UseCaseCapabilities(
@@ -1,17 +1,36 @@
package de.softwareapp_hb.privateqrscanner.ui.screens 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -20,14 +39,22 @@ import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp
import de.softwareapp_hb.privateqrscanner.R import de.softwareapp_hb.privateqrscanner.R
import de.softwareapp_hb.privateqrscanner.domain.ScanRecord import de.softwareapp_hb.privateqrscanner.domain.ScanRecord
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
import de.softwareapp_hb.privateqrscanner.ui.capabilities 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.HistoryExportFormatter
import de.softwareapp_hb.privateqrscanner.util.Intents import de.softwareapp_hb.privateqrscanner.util.Intents
import java.text.DateFormat import java.text.DateFormat
@@ -90,55 +117,97 @@ fun HistoryScreen(
) )
} }
Column( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .background(PrivateQrColors.AppBackground)
verticalArrangement = Arrangement.Top .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( OutlinedTextField(
value = query, value = query,
onValueChange = onQueryChange, onValueChange = onQueryChange,
modifier = Modifier.fillMaxWidth(), 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) { if (capabilities.allowHistoryExport) {
TextButton( ExportButton(
text = stringResource(R.string.share_txt),
enabled = history.isNotEmpty(),
onClick = { onClick = {
val exportText = HistoryExportFormatter.formatText(history) val exportText = HistoryExportFormatter.formatText(history)
Intents.shareContent(context, exportText, "text/plain") 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 = { onClick = {
val exportCsv = HistoryExportFormatter.formatCsv(history) val exportCsv = HistoryExportFormatter.formatCsv(history)
Intents.shareContent(context, exportCsv, "text/csv") 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 = { onClick = {
val exportJson = HistoryExportFormatter.formatJson(history) val exportJson = HistoryExportFormatter.formatJson(history)
Intents.shareContent(context, exportJson, "application/json") 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 }) { Spacer(modifier = Modifier.height(4.dp))
Text(stringResource(R.string.delete_all))
}
} }
LazyColumn { if (history.isEmpty()) {
item {
EmptyHistoryCard()
}
} else {
items(history, key = { it.id }) { item -> items(history, key = { it.id }) { item ->
HistoryRow( HistoryRow(
item = item, 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, state = dismissState,
backgroundContent = {}, backgroundContent = {},
content = { content = {
Column(modifier = Modifier Card(
modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onOpenDetails() } .clickable { onOpenDetails() },
.padding(vertical = 12.dp)) { colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
Text(text = item.type) 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(
text = if (item.isBase64Encoded()) { text = if (item.isBase64Encoded()) {
stringResource(R.string.base64_encoded_inline, item.content) stringResource(R.string.base64_encoded_inline, item.content)
} else { } else {
item.content 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 { private fun ScanRecord.isBase64Encoded(): Boolean {
return type.contains("base64", ignoreCase = true) return type.contains("base64", ignoreCase = true)
} }
@@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.softwareapp_hb.privateqrscanner.R import de.softwareapp_hb.privateqrscanner.R
import de.softwareapp_hb.privateqrscanner.domain.ScanResult 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.ParsedContact
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
import de.softwareapp_hb.privateqrscanner.util.UrlRiskScorer import de.softwareapp_hb.privateqrscanner.util.UrlRiskScorer
@@ -60,39 +61,59 @@ internal fun ResultVisualCard(
val fields = remember(result) { buildResultFields(result) } val fields = remember(result) { buildResultFields(result) }
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF2F7FF)), colors = CardDefaults.cardColors(containerColor = PrivateQrColors.SoftSurface),
shape = RoundedCornerShape(14.dp) shape = RoundedCornerShape(26.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(14.dp), modifier = Modifier.padding(22.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
if (!result.isBase64Encoded && result.type == "WiFi") { if (!result.isBase64Encoded && result.type == "WiFi") {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = Icons.Default.Wifi, imageVector = Icons.Default.Wifi,
contentDescription = null, contentDescription = null,
tint = Color(0xFF1D4ED8) tint = PrivateQrColors.Teal700
) )
Text( Text(
text = "Wi-Fi", text = "Wi-Fi",
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.headlineSmall
) )
} }
} else { } else {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = result.displayType, 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) { if (result.isBase64Encoded) {
Text( Text(
text = stringResource(R.string.base64_encoded_notice), text = stringResource(R.string.base64_encoded_notice),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = Color(0xFF4F6277) color = PrivateQrColors.TextSecondary
) )
} }
if (fields.isEmpty()) { if (fields.isEmpty()) {
@@ -106,7 +127,7 @@ internal fun ResultVisualCard(
Text( Text(
text = field.label, text = field.label,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color(0xFF4F6277) color = PrivateQrColors.TextSecondary
) )
val isClickableUrl = result.type == "URL" && val isClickableUrl = result.type == "URL" &&
field.label == "Link" && field.label == "Link" &&
@@ -114,7 +135,7 @@ internal fun ResultVisualCard(
Text( Text(
text = field.value, text = field.value,
style = MaterialTheme.typography.bodyMedium, 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, textDecoration = if (isClickableUrl) TextDecoration.Underline else null,
modifier = if (isClickableUrl) { modifier = if (isClickableUrl) {
Modifier.clickable { onOpenUrl(field.value) } 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.material.icons.filled.ViewModule
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
@@ -57,6 +59,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke 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 de.softwareapp_hb.privateqrscanner.ui.UseCaseView
import com.clean.scanner.ui.components.CameraPreview import com.clean.scanner.ui.components.CameraPreview
import de.softwareapp_hb.privateqrscanner.ui.capabilities 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.ClipboardUtil
import de.softwareapp_hb.privateqrscanner.util.Intents import de.softwareapp_hb.privateqrscanner.util.Intents
import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers import de.softwareapp_hb.privateqrscanner.util.ScanContentParsers
@@ -317,14 +321,15 @@ fun ScannerScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(PrivateQrColors.Deep)
.onSizeChanged { containerSize = it } .onSizeChanged { containerSize = it }
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val viewW = containerSize.width.toFloat() val viewW = containerSize.width.toFloat()
val viewH = containerSize.height.toFloat() val viewH = containerSize.height.toFloat()
val galleryOpen = imageScanPreviewUri != null val galleryOpen = imageScanPreviewUri != null
val aimW = viewW * 0.62f val aimW = viewW * 0.70f
val aimH = with(density) { 200.dp.toPx() } val aimH = with(density) { 230.dp.toPx() }
val aimLeft = (viewW - aimW) / 2f val aimLeft = (viewW - aimW) / 2f
val aimTop = (viewH - aimH) / 2f val aimTop = (viewH - aimH) / 2f
val aimRight = aimLeft + aimW 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()) { if (detectionBoxes.isNotEmpty()) {
Canvas( Canvas(
modifier = Modifier modifier = Modifier
@@ -421,15 +440,15 @@ fun ScannerScreen(
drawPath( drawPath(
path = outline, path = outline,
color = boxColor.copy(alpha = 0.96f), color = boxColor.copy(alpha = 0.96f),
style = Stroke(width = 4f) style = Stroke(width = 5.5f)
) )
} else if (right > left && bottom > top) { } else if (right > left && bottom > top) {
drawRoundRect( drawRoundRect(
color = boxColor.copy(alpha = 0.95f), color = boxColor.copy(alpha = 0.95f),
topLeft = Offset(left, top), topLeft = Offset(left, top),
size = Size(right - left, bottom - top), size = Size(right - left, bottom - top),
cornerRadius = CornerRadius(14f, 14f), cornerRadius = CornerRadius(22f, 22f),
style = Stroke(width = 4f) style = Stroke(width = 5.5f)
) )
} }
} }
@@ -439,22 +458,22 @@ fun ScannerScreen(
Canvas( Canvas(
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
.fillMaxWidth(0.62f) .fillMaxWidth(0.70f)
.height(200.dp) .height(230.dp)
) { ) {
val guideColor = when { val guideColor = when {
hasReadableInView -> Color(0xFF4AE3A3) hasReadableInView -> Color(0xFF4AE3A3)
hasPotentialInView -> Color(0xFFFFC857) hasPotentialInView -> PrivateQrColors.Warning
else -> Color(0xFF7CE6C6) else -> PrivateQrColors.Teal300
} }
drawRoundRect( drawRoundRect(
color = guideColor.copy(alpha = 0.08f), color = guideColor.copy(alpha = 0.12f),
cornerRadius = CornerRadius(22f, 22f) cornerRadius = CornerRadius(32f, 32f)
) )
drawRoundRect( drawRoundRect(
color = guideColor.copy(alpha = 0.90f), color = guideColor.copy(alpha = 0.90f),
cornerRadius = CornerRadius(22f, 22f), cornerRadius = CornerRadius(32f, 32f),
style = Stroke(width = 3.5f) style = Stroke(width = 5f)
) )
} }
@@ -465,15 +484,16 @@ fun ScannerScreen(
else -> stringResource(R.string.aim_center_hint) else -> stringResource(R.string.aim_center_hint)
}, },
color = Color.White, color = Color.White,
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.padding(bottom = if (isBatchModeActive) 190.dp else 56.dp) .padding(bottom = if (isBatchModeActive) 190.dp else 56.dp)
.background( .background(
color = Color.Black.copy(alpha = 0.35f), color = Color.Black.copy(alpha = 0.45f),
shape = RoundedCornerShape(18.dp) shape = RoundedCornerShape(24.dp)
) )
.padding(horizontal = 14.dp, vertical = 8.dp) .padding(horizontal = 18.dp, vertical = 10.dp)
) )
if (capabilities.allowScanFromImage) { if (capabilities.allowScanFromImage) {
@@ -483,8 +503,8 @@ fun ScannerScreen(
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(top = 12.dp, end = 12.dp) .padding(top = 12.dp, end = 12.dp)
.background( .background(
color = Color.Black.copy(alpha = 0.35f), color = Color.Black.copy(alpha = 0.38f),
shape = RoundedCornerShape(14.dp) shape = RoundedCornerShape(18.dp)
) )
) { ) {
Icon( Icon(
@@ -501,8 +521,8 @@ fun ScannerScreen(
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(top = 12.dp, end = if (capabilities.allowScanFromImage) 64.dp else 12.dp) .padding(top = 12.dp, end = if (capabilities.allowScanFromImage) 64.dp else 12.dp)
.background( .background(
color = Color.Black.copy(alpha = 0.35f), color = Color.Black.copy(alpha = 0.38f),
shape = RoundedCornerShape(14.dp) shape = RoundedCornerShape(18.dp)
) )
) { ) {
Icon( Icon(
@@ -516,15 +536,16 @@ fun ScannerScreen(
Text( Text(
text = stringResource(useCaseView.titleRes), text = stringResource(useCaseView.titleRes),
color = Color.White, color = Color.White,
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.padding(top = 16.dp, start = 64.dp, end = 64.dp) .padding(top = 16.dp, start = 64.dp, end = 64.dp)
.background( .background(
color = Color.Black.copy(alpha = 0.4f), color = Color.Black.copy(alpha = 0.52f),
shape = RoundedCornerShape(14.dp) shape = RoundedCornerShape(22.dp)
) )
.padding(horizontal = 12.dp, vertical = 6.dp) .padding(horizontal = 18.dp, vertical = 10.dp)
) )
if (useCaseView == UseCaseView.EventTicketing) { if (useCaseView == UseCaseView.EventTicketing) {
Text( Text(
@@ -613,12 +634,16 @@ fun ScannerScreen(
if (lastResult.isBase64Encoded) null else ScanContentParsers.parseCalendarEvent(lastResult.content) 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(start = 20.dp, top = 4.dp, end = 20.dp, bottom = 28.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
ResultVisualCard( ResultVisualCard(
result = lastResult, result = lastResult,
@@ -642,31 +667,50 @@ fun ScannerScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.horizontalScroll(rememberScrollState()), .horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
if (capabilities.allowAddContact && parsedContact != null) { if (capabilities.allowAddContact && parsedContact != null) {
IconButton(onClick = { IconButton(
Intents.addContact(context, parsedContact, lastResult.content) onClick = { Intents.addContact(context, parsedContact, lastResult.content) },
}) { modifier = Modifier.background(
color = PrivateQrColors.AppBackground,
shape = RoundedCornerShape(50)
)
) {
Icon( Icon(
imageVector = Icons.Default.PersonAdd, imageVector = Icons.Default.PersonAdd,
contentDescription = stringResource(R.string.add_contact) contentDescription = stringResource(R.string.add_contact),
tint = PrivateQrColors.TextPrimary
) )
} }
} }
if (capabilities.allowCopy) { 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( Icon(
imageVector = Icons.Default.ContentCopy, imageVector = Icons.Default.ContentCopy,
contentDescription = stringResource(R.string.copy) contentDescription = stringResource(R.string.copy),
tint = PrivateQrColors.TextPrimary
) )
} }
} }
if (capabilities.allowShare) { 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( Icon(
imageVector = Icons.Default.Share, 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) { when (lastResult.type) {
"Phone" -> { "Phone" -> {
if (capabilities.allowDialPhone) { if (capabilities.allowDialPhone) {
Button(onClick = { Button(
onClick = {
Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content)) Intents.dialPhone(context, ScanContentParsers.extractPhoneNumber(lastResult.content))
}) { },
colors = ButtonDefaults.buttonColors(
containerColor = PrivateQrColors.Teal700
)
) {
Text(stringResource(R.string.call_number)) Text(stringResource(R.string.call_number))
} }
} }
@@ -687,10 +736,15 @@ fun ScannerScreen(
"SMS" -> { "SMS" -> {
if (capabilities.allowSendSms) { if (capabilities.allowSendSms) {
Button(onClick = { Button(
onClick = {
val smsData = ScanContentParsers.parseSms(lastResult.content) val smsData = ScanContentParsers.parseSms(lastResult.content)
Intents.sendSms(context, smsData.first, smsData.second) Intents.sendSms(context, smsData.first, smsData.second)
}) { },
colors = ButtonDefaults.buttonColors(
containerColor = PrivateQrColors.Teal700
)
) {
Text(stringResource(R.string.send_sms)) Text(stringResource(R.string.send_sms))
} }
} }
@@ -698,9 +752,14 @@ fun ScannerScreen(
"Email" -> { "Email" -> {
if (capabilities.allowSendEmail) { if (capabilities.allowSendEmail) {
Button(onClick = { Button(
onClick = {
Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null) Intents.sendEmail(context, ScanContentParsers.extractEmail(lastResult.content), null)
}) { },
colors = ButtonDefaults.buttonColors(
containerColor = PrivateQrColors.Teal700
)
) {
Text(stringResource(R.string.send_email)) Text(stringResource(R.string.send_email))
} }
} }
@@ -708,7 +767,12 @@ fun ScannerScreen(
"WiFi" -> { "WiFi" -> {
if (capabilities.allowOpenWifiSettings) { 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)) Text(stringResource(R.string.open_wifi_settings))
} }
} }
@@ -716,9 +780,14 @@ fun ScannerScreen(
"Calendar" -> { "Calendar" -> {
if (capabilities.allowAddCalendarEvent) { if (capabilities.allowAddCalendarEvent) {
Button(onClick = { Button(
onClick = {
Intents.addCalendarEvent(context, parsedEvent, lastResult.content) Intents.addCalendarEvent(context, parsedEvent, lastResult.content)
}) { },
colors = ButtonDefaults.buttonColors(
containerColor = PrivateQrColors.Teal700
)
) {
Text(stringResource(R.string.add_calendar_event)) Text(stringResource(R.string.add_calendar_event))
} }
} }
@@ -1,25 +1,44 @@
package de.softwareapp_hb.privateqrscanner.ui.screens package de.softwareapp_hb.privateqrscanner.ui.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.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.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.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.softwareapp_hb.privateqrscanner.R import de.softwareapp_hb.privateqrscanner.R
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
import de.softwareapp_hb.privateqrscanner.util.InAppReviewRequester import de.softwareapp_hb.privateqrscanner.util.InAppReviewRequester
@Composable @Composable
@@ -93,11 +112,29 @@ fun SettingsScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(24.dp), .background(PrivateQrColors.AppBackground)
verticalArrangement = Arrangement.Top .verticalScroll(rememberScrollState())
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(18.dp)
) { ) {
Text(text = stringResource(R.string.save_history)) SettingsHeader()
Switch(
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, checked = historyEnabled,
onCheckedChange = { enabled -> onCheckedChange = { enabled ->
if (!enabled && historyEnabled) { if (!enabled && historyEnabled) {
@@ -107,36 +144,184 @@ fun SettingsScreen(
} }
} }
) )
SettingsDivider()
Spacer(modifier = Modifier.height(16.dp)) SettingsToggleRow(
title = stringResource(R.string.security_warnings),
Text(text = stringResource(R.string.security_warnings)) checked = warningsEnabled,
Switch(checked = warningsEnabled, onCheckedChange = onWarningsToggle) onCheckedChange = onWarningsToggle
)
Spacer(modifier = Modifier.height(16.dp)) SettingsDivider()
SettingsToggleRow(
Text(text = stringResource(R.string.scan_feedback)) title = stringResource(R.string.scan_feedback),
Switch(checked = scanFeedbackEnabled, onCheckedChange = onScanFeedbackToggle) 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))
} }
Spacer(modifier = Modifier.height(24.dp)) Card(
Text(text = stringResource(R.string.about)) modifier = Modifier.fillMaxWidth(),
Text(text = stringResource(R.string.version)) colors = CardDefaults.cardColors(containerColor = PrivateQrColors.Surface),
Text(text = stringResource(R.string.licenses)) shape = RoundedCornerShape(28.dp),
Text(text = stringResource(R.string.contact)) elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
TextButton(onClick = { showPrivacyPolicy.value = true }) { ) {
Text(text = stringResource(R.string.privacy_policy)) 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)) Spacer(modifier = Modifier.height(12.dp))
TextButton(onClick = { InAppReviewRequester.requestReview(context) }) { }
Text(text = stringResource(R.string.review_app)) }
@Composable
private fun SettingsHeader() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_legacy),
contentDescription = null,
modifier = Modifier.size(64.dp)
)
Column {
Text(
text = stringResource(R.string.app_name),
color = PrivateQrColors.TextPrimary,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.ExtraBold
)
Text(
text = "Local privacy controls",
color = PrivateQrColors.TextSecondary,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold
)
}
}
Spacer(modifier = Modifier.height(18.dp))
Card(
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
shape = RoundedCornerShape(30.dp)
) {
Column(
modifier = Modifier
.background(
Brush.linearGradient(
colors = listOf(PrivateQrColors.Navy, PrivateQrColors.Teal900)
)
)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Control what stays saved",
color = PrivateQrColors.Surface,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.ExtraBold
)
Text(
text = "History, warnings, and feedback are optional device-local settings.",
color = PrivateQrColors.Mint,
style = MaterialTheme.typography.titleMedium
)
} }
} }
} }
@Composable
private fun SettingsToggleRow(
title: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(74.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
color = PrivateQrColors.TextPrimary,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.ExtraBold
)
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = PrivateQrColors.Surface,
checkedTrackColor = PrivateQrColors.Teal700,
uncheckedThumbColor = PrivateQrColors.Surface,
uncheckedTrackColor = PrivateQrColors.TextSecondary.copy(alpha = 0.35f)
)
)
}
}
@Composable
private fun SettingsDivider() {
HorizontalDivider(color = PrivateQrColors.Divider)
}
@Composable
private fun InfoLine(text: String) {
Text(
text = text,
color = PrivateQrColors.TextSecondary,
style = MaterialTheme.typography.bodyMedium
)
}
@@ -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 package de.softwareapp_hb.privateqrscanner.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColors = lightColorScheme() private val LightColors = lightColorScheme(
private val DarkColors = darkColorScheme() 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 @Composable
fun CleanScannerTheme(content: @Composable () -> Unit) { fun CleanScannerTheme(content: @Composable () -> Unit) {
val darkTheme = isSystemInDarkTheme() val darkTheme = isSystemInDarkTheme()
val context = LocalContext.current val colorScheme = if (darkTheme) DarkColors else LightColors
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} else {
if (darkTheme) DarkColors else LightColors
}
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
@@ -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