nicer visual aperance
This commit is contained in:
@@ -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,8 @@ fun UseCaseView.capabilities(): UseCaseCapabilities {
|
||||
allowBatchMode = false,
|
||||
allowCopy = true,
|
||||
allowShare = true,
|
||||
allowOpenUrl = true
|
||||
allowOpenUrl = 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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,44 @@
|
||||
package de.softwareapp_hb.privateqrscanner.ui.screens
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.softwareapp_hb.privateqrscanner.R
|
||||
import de.softwareapp_hb.privateqrscanner.ui.UseCaseView
|
||||
import de.softwareapp_hb.privateqrscanner.ui.theme.PrivateQrColors
|
||||
import de.softwareapp_hb.privateqrscanner.util.InAppReviewRequester
|
||||
|
||||
@Composable
|
||||
@@ -93,11 +112,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 +144,184 @@ 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 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,
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user